rubyn-code 0.1.0 → 0.2.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 +269 -467
- data/db/migrations/009_create_teams.sql +6 -6
- data/db/migrations/011_fix_mailbox_messages_columns.rb +35 -0
- data/db/migrations/012_expand_mailbox_message_types.rb +37 -0
- data/exe/rubyn-code +1 -1
- data/lib/rubyn_code/agent/RUBYN.md +17 -0
- data/lib/rubyn_code/agent/conversation.rb +68 -19
- data/lib/rubyn_code/agent/loop.rb +312 -54
- data/lib/rubyn_code/agent/loop_detector.rb +6 -6
- data/lib/rubyn_code/auth/RUBYN.md +19 -0
- data/lib/rubyn_code/auth/oauth.rb +40 -35
- data/lib/rubyn_code/auth/server.rb +16 -12
- data/lib/rubyn_code/auth/token_store.rb +22 -22
- data/lib/rubyn_code/autonomous/RUBYN.md +14 -0
- data/lib/rubyn_code/autonomous/daemon.rb +115 -79
- data/lib/rubyn_code/autonomous/idle_poller.rb +4 -8
- data/lib/rubyn_code/autonomous/task_claimer.rb +11 -11
- data/lib/rubyn_code/background/RUBYN.md +13 -0
- data/lib/rubyn_code/background/notifier.rb +0 -2
- data/lib/rubyn_code/background/worker.rb +60 -15
- data/lib/rubyn_code/cli/RUBYN.md +30 -0
- data/lib/rubyn_code/cli/app.rb +85 -9
- data/lib/rubyn_code/cli/commands/RUBYN.md +133 -0
- data/lib/rubyn_code/cli/commands/base.rb +53 -0
- data/lib/rubyn_code/cli/commands/budget.rb +24 -0
- data/lib/rubyn_code/cli/commands/clear.rb +16 -0
- data/lib/rubyn_code/cli/commands/compact.rb +21 -0
- data/lib/rubyn_code/cli/commands/context.rb +44 -0
- data/lib/rubyn_code/cli/commands/context_info.rb +56 -0
- data/lib/rubyn_code/cli/commands/cost.rb +23 -0
- data/lib/rubyn_code/cli/commands/diff.rb +30 -0
- data/lib/rubyn_code/cli/commands/doctor.rb +112 -0
- data/lib/rubyn_code/cli/commands/help.rb +41 -0
- data/lib/rubyn_code/cli/commands/model.rb +37 -0
- data/lib/rubyn_code/cli/commands/plan.rb +22 -0
- data/lib/rubyn_code/cli/commands/quit.rb +17 -0
- data/lib/rubyn_code/cli/commands/registry.rb +64 -0
- data/lib/rubyn_code/cli/commands/resume.rb +51 -0
- data/lib/rubyn_code/cli/commands/review.rb +26 -0
- data/lib/rubyn_code/cli/commands/skill.rb +32 -0
- data/lib/rubyn_code/cli/commands/spawn.rb +24 -0
- data/lib/rubyn_code/cli/commands/tasks.rb +32 -0
- data/lib/rubyn_code/cli/commands/tokens.rb +76 -0
- data/lib/rubyn_code/cli/commands/undo.rb +17 -0
- data/lib/rubyn_code/cli/commands/version.rb +16 -0
- data/lib/rubyn_code/cli/daemon_runner.rb +129 -0
- data/lib/rubyn_code/cli/input_handler.rb +20 -23
- data/lib/rubyn_code/cli/renderer.rb +25 -27
- data/lib/rubyn_code/cli/repl.rb +161 -194
- data/lib/rubyn_code/cli/setup.rb +117 -0
- data/lib/rubyn_code/cli/spinner.rb +40 -40
- data/lib/rubyn_code/cli/stream_formatter.rb +29 -28
- data/lib/rubyn_code/cli/version_check.rb +94 -0
- data/lib/rubyn_code/config/RUBYN.md +14 -0
- data/lib/rubyn_code/config/defaults.rb +28 -19
- data/lib/rubyn_code/config/project_config.rb +7 -9
- data/lib/rubyn_code/config/settings.rb +3 -3
- data/lib/rubyn_code/context/RUBYN.md +20 -0
- data/lib/rubyn_code/context/auto_compact.rb +7 -7
- data/lib/rubyn_code/context/compactor.rb +2 -2
- data/lib/rubyn_code/context/context_collapse.rb +45 -0
- data/lib/rubyn_code/context/manager.rb +20 -3
- data/lib/rubyn_code/context/manual_compact.rb +7 -7
- data/lib/rubyn_code/context/micro_compact.rb +12 -12
- data/lib/rubyn_code/db/RUBYN.md +40 -0
- data/lib/rubyn_code/db/connection.rb +13 -13
- data/lib/rubyn_code/db/migrator.rb +67 -27
- data/lib/rubyn_code/db/schema.rb +6 -6
- data/lib/rubyn_code/debug.rb +74 -0
- data/lib/rubyn_code/hooks/RUBYN.md +17 -0
- data/lib/rubyn_code/hooks/built_in.rb +9 -9
- data/lib/rubyn_code/hooks/registry.rb +5 -5
- data/lib/rubyn_code/hooks/runner.rb +1 -1
- data/lib/rubyn_code/hooks/user_hooks.rb +16 -16
- data/lib/rubyn_code/learning/RUBYN.md +16 -0
- data/lib/rubyn_code/learning/extractor.rb +22 -22
- data/lib/rubyn_code/learning/injector.rb +17 -18
- data/lib/rubyn_code/learning/instinct.rb +18 -14
- data/lib/rubyn_code/llm/RUBYN.md +15 -0
- data/lib/rubyn_code/llm/client.rb +121 -55
- data/lib/rubyn_code/llm/message_builder.rb +19 -15
- data/lib/rubyn_code/llm/streaming.rb +80 -50
- data/lib/rubyn_code/mcp/RUBYN.md +21 -0
- data/lib/rubyn_code/mcp/client.rb +25 -24
- data/lib/rubyn_code/mcp/config.rb +7 -7
- data/lib/rubyn_code/mcp/sse_transport.rb +27 -26
- data/lib/rubyn_code/mcp/stdio_transport.rb +22 -19
- data/lib/rubyn_code/mcp/tool_bridge.rb +32 -32
- data/lib/rubyn_code/memory/RUBYN.md +17 -0
- data/lib/rubyn_code/memory/models.rb +3 -3
- data/lib/rubyn_code/memory/search.rb +17 -17
- data/lib/rubyn_code/memory/session_persistence.rb +49 -34
- data/lib/rubyn_code/memory/store.rb +17 -17
- data/lib/rubyn_code/observability/RUBYN.md +19 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +16 -15
- data/lib/rubyn_code/observability/cost_calculator.rb +3 -3
- data/lib/rubyn_code/observability/token_counter.rb +1 -1
- data/lib/rubyn_code/observability/usage_reporter.rb +35 -35
- data/lib/rubyn_code/output/RUBYN.md +11 -0
- data/lib/rubyn_code/output/diff_renderer.rb +6 -6
- data/lib/rubyn_code/output/formatter.rb +4 -4
- data/lib/rubyn_code/permissions/RUBYN.md +17 -0
- data/lib/rubyn_code/permissions/prompter.rb +8 -8
- data/lib/rubyn_code/protocols/RUBYN.md +14 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +1 -1
- data/lib/rubyn_code/protocols/plan_approval.rb +9 -9
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +9 -11
- data/lib/rubyn_code/skills/RUBYN.md +19 -0
- data/lib/rubyn_code/skills/catalog.rb +7 -7
- data/lib/rubyn_code/skills/document.rb +15 -15
- data/lib/rubyn_code/skills/loader.rb +6 -8
- data/lib/rubyn_code/sub_agents/RUBYN.md +12 -0
- data/lib/rubyn_code/sub_agents/runner.rb +15 -15
- data/lib/rubyn_code/sub_agents/summarizer.rb +1 -1
- data/lib/rubyn_code/tasks/RUBYN.md +13 -0
- data/lib/rubyn_code/tasks/dag.rb +12 -16
- data/lib/rubyn_code/tasks/manager.rb +24 -24
- data/lib/rubyn_code/tasks/models.rb +4 -4
- data/lib/rubyn_code/teams/RUBYN.md +14 -0
- data/lib/rubyn_code/teams/mailbox.rb +38 -18
- data/lib/rubyn_code/teams/manager.rb +19 -19
- data/lib/rubyn_code/teams/teammate.rb +3 -4
- data/lib/rubyn_code/tools/RUBYN.md +38 -0
- data/lib/rubyn_code/tools/background_run.rb +9 -11
- data/lib/rubyn_code/tools/base.rb +54 -3
- data/lib/rubyn_code/tools/bash.rb +16 -34
- data/lib/rubyn_code/tools/bundle_add.rb +10 -12
- data/lib/rubyn_code/tools/bundle_install.rb +9 -11
- data/lib/rubyn_code/tools/compact.rb +10 -9
- data/lib/rubyn_code/tools/db_migrate.rb +17 -15
- data/lib/rubyn_code/tools/edit_file.rb +12 -12
- data/lib/rubyn_code/tools/executor.rb +9 -4
- data/lib/rubyn_code/tools/git_commit.rb +29 -34
- data/lib/rubyn_code/tools/git_diff.rb +17 -18
- data/lib/rubyn_code/tools/git_log.rb +17 -19
- data/lib/rubyn_code/tools/git_status.rb +18 -20
- data/lib/rubyn_code/tools/glob.rb +7 -9
- data/lib/rubyn_code/tools/grep.rb +11 -9
- data/lib/rubyn_code/tools/load_skill.rb +7 -7
- data/lib/rubyn_code/tools/memory_search.rb +13 -12
- data/lib/rubyn_code/tools/memory_write.rb +14 -12
- data/lib/rubyn_code/tools/rails_generate.rb +16 -16
- data/lib/rubyn_code/tools/read_file.rb +8 -7
- data/lib/rubyn_code/tools/read_inbox.rb +5 -5
- data/lib/rubyn_code/tools/registry.rb +2 -2
- data/lib/rubyn_code/tools/review_pr.rb +55 -55
- data/lib/rubyn_code/tools/run_specs.rb +20 -19
- data/lib/rubyn_code/tools/schema.rb +9 -11
- data/lib/rubyn_code/tools/send_message.rb +10 -10
- data/lib/rubyn_code/tools/spawn_agent.rb +51 -23
- data/lib/rubyn_code/tools/spawn_teammate.rb +21 -21
- data/lib/rubyn_code/tools/task.rb +28 -28
- data/lib/rubyn_code/tools/web_fetch.rb +46 -31
- data/lib/rubyn_code/tools/web_search.rb +64 -66
- data/lib/rubyn_code/tools/write_file.rb +7 -6
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +136 -105
- metadata +94 -21
data/lib/rubyn_code/cli/repl.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'reline'
|
|
4
4
|
|
|
5
5
|
module RubynCode
|
|
6
6
|
module CLI
|
|
@@ -14,13 +14,19 @@ module RubynCode
|
|
|
14
14
|
@running = true
|
|
15
15
|
@session_id = session_id
|
|
16
16
|
@permission_tier = yolo ? :unrestricted : :allow_read
|
|
17
|
+
@plan_mode = false
|
|
17
18
|
|
|
18
|
-
setup_readline!
|
|
19
19
|
setup_components!
|
|
20
|
+
setup_command_registry!
|
|
21
|
+
setup_readline!
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def run
|
|
25
|
+
@version_check = VersionCheck.new(renderer: @renderer)
|
|
26
|
+
@version_check.start
|
|
27
|
+
|
|
23
28
|
@renderer.welcome
|
|
29
|
+
@version_check.notify
|
|
24
30
|
|
|
25
31
|
at_exit { shutdown! }
|
|
26
32
|
|
|
@@ -43,7 +49,7 @@ module RubynCode
|
|
|
43
49
|
end
|
|
44
50
|
@last_interrupt = now
|
|
45
51
|
puts
|
|
46
|
-
@renderer.info(
|
|
52
|
+
@renderer.info('Press Ctrl-C again to exit, or type /quit')
|
|
47
53
|
end
|
|
48
54
|
end
|
|
49
55
|
|
|
@@ -52,6 +58,8 @@ module RubynCode
|
|
|
52
58
|
|
|
53
59
|
private
|
|
54
60
|
|
|
61
|
+
# ── Component Setup ──────────────────────────────────────────────
|
|
62
|
+
|
|
55
63
|
def setup_components!
|
|
56
64
|
ensure_home_dir!
|
|
57
65
|
@db = DB::Connection.instance
|
|
@@ -80,7 +88,7 @@ module RubynCode
|
|
|
80
88
|
@tool_executor.db = @db
|
|
81
89
|
@sub_agent_tool_count = 0
|
|
82
90
|
@in_sub_agent = false
|
|
83
|
-
@tool_executor.on_agent_status =
|
|
91
|
+
@tool_executor.on_agent_status = lambda { |type, msg|
|
|
84
92
|
case type
|
|
85
93
|
when :started
|
|
86
94
|
@spinner.stop
|
|
@@ -113,7 +121,7 @@ module RubynCode
|
|
|
113
121
|
budget_enforcer: @budget_enforcer,
|
|
114
122
|
background_manager: @background_worker,
|
|
115
123
|
stall_detector: @stall_detector,
|
|
116
|
-
on_tool_call:
|
|
124
|
+
on_tool_call: lambda { |name, params|
|
|
117
125
|
@spinner.stop
|
|
118
126
|
unless @streaming_first_chunk
|
|
119
127
|
@stream_formatter&.flush
|
|
@@ -123,69 +131,138 @@ module RubynCode
|
|
|
123
131
|
end
|
|
124
132
|
@renderer.tool_call(name, params)
|
|
125
133
|
},
|
|
126
|
-
on_tool_result:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
end
|
|
130
|
-
@streaming_first_chunk = true
|
|
131
|
-
@spinner.start unless @in_sub_agent
|
|
134
|
+
on_tool_result: lambda { |name, result, _is_error = false|
|
|
135
|
+
@renderer.tool_result(name, result)
|
|
136
|
+
@spinner.start
|
|
132
137
|
},
|
|
133
|
-
on_text:
|
|
138
|
+
on_text: lambda { |text|
|
|
139
|
+
@spinner.stop
|
|
134
140
|
if @streaming_first_chunk
|
|
135
|
-
@
|
|
141
|
+
@stream_formatter = StreamFormatter.new
|
|
142
|
+
puts
|
|
136
143
|
@streaming_first_chunk = false
|
|
137
|
-
@stream_formatter ||= StreamFormatter.new(@renderer)
|
|
138
144
|
end
|
|
139
|
-
@
|
|
140
|
-
@stream_formatter.feed(text)
|
|
145
|
+
@stream_formatter&.feed(text)
|
|
141
146
|
},
|
|
142
147
|
skill_loader: @skill_loader,
|
|
143
148
|
project_root: @project_root
|
|
144
149
|
)
|
|
150
|
+
end
|
|
145
151
|
|
|
146
|
-
|
|
152
|
+
# ── Command Registry ─────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
def setup_command_registry!
|
|
155
|
+
@command_registry = Commands::Registry.new
|
|
156
|
+
|
|
157
|
+
# Register all commands
|
|
158
|
+
[
|
|
159
|
+
Commands::Help,
|
|
160
|
+
Commands::Quit,
|
|
161
|
+
Commands::Compact,
|
|
162
|
+
Commands::Cost,
|
|
163
|
+
Commands::Clear,
|
|
164
|
+
Commands::Undo,
|
|
165
|
+
Commands::Tasks,
|
|
166
|
+
Commands::Budget,
|
|
167
|
+
Commands::Skill,
|
|
168
|
+
Commands::Version,
|
|
169
|
+
Commands::Review,
|
|
170
|
+
Commands::Resume,
|
|
171
|
+
Commands::Spawn,
|
|
172
|
+
Commands::Doctor,
|
|
173
|
+
Commands::Tokens,
|
|
174
|
+
Commands::Plan,
|
|
175
|
+
Commands::ContextInfo,
|
|
176
|
+
Commands::Diff,
|
|
177
|
+
Commands::Model
|
|
178
|
+
].each { |cmd| @command_registry.register(cmd) }
|
|
179
|
+
|
|
180
|
+
# Give Help access to the registry for listing commands
|
|
181
|
+
Commands::Help.registry = @command_registry
|
|
182
|
+
|
|
183
|
+
# Update input handler to use registry for parsing
|
|
184
|
+
@input_handler = InputHandler.new(command_registry: @command_registry)
|
|
147
185
|
end
|
|
148
186
|
|
|
187
|
+
# ── Command Dispatch ─────────────────────────────────────────────
|
|
188
|
+
|
|
149
189
|
def handle_command(command)
|
|
150
190
|
case command.action
|
|
151
191
|
when :quit
|
|
152
192
|
@running = false
|
|
153
193
|
when :message
|
|
154
194
|
handle_message(command.args.first)
|
|
155
|
-
when :compact
|
|
156
|
-
handle_compact(command.args.first)
|
|
157
|
-
when :cost
|
|
158
|
-
handle_cost
|
|
159
|
-
when :clear
|
|
160
|
-
system("clear")
|
|
161
|
-
when :undo
|
|
162
|
-
@conversation.undo_last!
|
|
163
|
-
@renderer.info("Last exchange removed.")
|
|
164
|
-
when :help
|
|
165
|
-
display_help
|
|
166
|
-
when :tasks
|
|
167
|
-
handle_tasks
|
|
168
|
-
when :budget
|
|
169
|
-
handle_budget(command.args.first)
|
|
170
|
-
when :skill
|
|
171
|
-
handle_skill(command.args.first)
|
|
172
|
-
when :version
|
|
173
|
-
@renderer.info("Rubyn Code v#{RubynCode::VERSION}")
|
|
174
|
-
when :review
|
|
175
|
-
handle_review(command.args)
|
|
176
|
-
when :spawn_teammate
|
|
177
|
-
handle_spawn_teammate(command.args)
|
|
178
|
-
when :resume
|
|
179
|
-
handle_resume(command.args.first)
|
|
180
195
|
when :empty
|
|
181
196
|
nil
|
|
182
197
|
when :list_commands
|
|
183
198
|
display_commands
|
|
184
199
|
when :unknown_command
|
|
185
200
|
@renderer.warning("Unknown command: #{command.args.first}. Type / to see available commands.")
|
|
201
|
+
when :slash_command
|
|
202
|
+
dispatch_slash_command(command.args[0], command.args[1..])
|
|
186
203
|
end
|
|
187
204
|
end
|
|
188
205
|
|
|
206
|
+
def dispatch_slash_command(name, args)
|
|
207
|
+
ctx = build_context
|
|
208
|
+
result = @command_registry.dispatch(name, args, ctx)
|
|
209
|
+
|
|
210
|
+
case result
|
|
211
|
+
when :quit
|
|
212
|
+
@running = false
|
|
213
|
+
when :unknown
|
|
214
|
+
@renderer.warning("Unknown command: #{name}. Type / to see available commands.")
|
|
215
|
+
when Hash
|
|
216
|
+
handle_command_result(result)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def build_context
|
|
221
|
+
Commands::Context.new(
|
|
222
|
+
renderer: @renderer,
|
|
223
|
+
conversation: @conversation,
|
|
224
|
+
agent_loop: @agent_loop,
|
|
225
|
+
context_manager: @context_manager,
|
|
226
|
+
budget_enforcer: @budget_enforcer,
|
|
227
|
+
llm_client: @llm_client,
|
|
228
|
+
db: @db,
|
|
229
|
+
session_id: current_session_id,
|
|
230
|
+
project_root: @project_root,
|
|
231
|
+
skill_loader: @skill_loader,
|
|
232
|
+
session_persistence: @session_persistence,
|
|
233
|
+
background_worker: @background_worker,
|
|
234
|
+
permission_tier: @permission_tier,
|
|
235
|
+
plan_mode: @plan_mode,
|
|
236
|
+
message_handler: method(:handle_message)
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Handle structured results from commands that need to mutate REPL state
|
|
241
|
+
def handle_command_result(result)
|
|
242
|
+
case result
|
|
243
|
+
in { action: :set_budget, amount: Float => amount }
|
|
244
|
+
@budget_enforcer = Observability::BudgetEnforcer.new(
|
|
245
|
+
@db,
|
|
246
|
+
session_id: current_session_id,
|
|
247
|
+
session_limit: amount
|
|
248
|
+
)
|
|
249
|
+
in { action: :set_plan_mode, enabled: true | false => enabled }
|
|
250
|
+
@plan_mode = enabled
|
|
251
|
+
@agent_loop.plan_mode = enabled if @agent_loop.respond_to?(:plan_mode=)
|
|
252
|
+
in { action: :set_session_id, session_id: String => sid }
|
|
253
|
+
@session_id = sid
|
|
254
|
+
in { action: :set_model, model: String => model }
|
|
255
|
+
@llm_client.model = model if @llm_client.respond_to?(:model=)
|
|
256
|
+
@renderer.info("Model set to #{model}")
|
|
257
|
+
in { action: :spawn_teammate, name: String => name, role: String => role }
|
|
258
|
+
spawn_teammate(name, role)
|
|
259
|
+
else
|
|
260
|
+
# Unknown result hash — ignore
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# ── Message Handling ─────────────────────────────────────────────
|
|
265
|
+
|
|
189
266
|
def handle_message(input)
|
|
190
267
|
@spinner.start
|
|
191
268
|
@streaming_first_chunk = true
|
|
@@ -210,105 +287,9 @@ module RubynCode
|
|
|
210
287
|
@renderer.error("Error: #{e.message}")
|
|
211
288
|
end
|
|
212
289
|
|
|
213
|
-
|
|
214
|
-
@spinner.start("Compacting context...")
|
|
215
|
-
compactor = Context::Compactor.new(llm_client: @llm_client)
|
|
216
|
-
new_messages = compactor.manual_compact!(@conversation.messages, focus: focus)
|
|
217
|
-
@conversation.replace!(new_messages)
|
|
218
|
-
@spinner.success
|
|
219
|
-
@renderer.info("Context compacted. #{@conversation.length} messages remaining.")
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def handle_cost
|
|
223
|
-
@renderer.cost_summary(
|
|
224
|
-
session_cost: @budget_enforcer.session_cost,
|
|
225
|
-
daily_cost: @budget_enforcer.daily_cost,
|
|
226
|
-
tokens: {
|
|
227
|
-
input: @context_manager.total_input_tokens,
|
|
228
|
-
output: @context_manager.total_output_tokens
|
|
229
|
-
}
|
|
230
|
-
)
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def handle_tasks
|
|
234
|
-
task_manager = Tasks::Manager.new(@db)
|
|
235
|
-
tasks = task_manager.list
|
|
236
|
-
if tasks.empty?
|
|
237
|
-
@renderer.info("No tasks.")
|
|
238
|
-
else
|
|
239
|
-
tasks.each do |t|
|
|
240
|
-
status_color = case t[:status]
|
|
241
|
-
when "completed" then :green
|
|
242
|
-
when "in_progress" then :yellow
|
|
243
|
-
when "blocked" then :red
|
|
244
|
-
else :white
|
|
245
|
-
end
|
|
246
|
-
puts " [#{t[:status]}] #{t[:title]} (#{t[:id][0..7]})"
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def handle_budget(amount)
|
|
252
|
-
if amount
|
|
253
|
-
@budget_enforcer = Observability::BudgetEnforcer.new(
|
|
254
|
-
@db,
|
|
255
|
-
session_id: current_session_id,
|
|
256
|
-
session_limit: amount.to_f
|
|
257
|
-
)
|
|
258
|
-
@renderer.info("Session budget set to $#{amount}")
|
|
259
|
-
else
|
|
260
|
-
@renderer.info("Remaining budget: $#{'%.4f' % @budget_enforcer.remaining_budget}")
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def handle_skill(name)
|
|
265
|
-
if name
|
|
266
|
-
content = @skill_loader.load(name)
|
|
267
|
-
@renderer.info("Loaded skill: #{name}")
|
|
268
|
-
@conversation.add_user_message("<skill>#{content}</skill>")
|
|
269
|
-
else
|
|
270
|
-
@renderer.info("Available skills:")
|
|
271
|
-
puts @skill_loader.descriptions_for_prompt
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def handle_resume(session_id)
|
|
276
|
-
if session_id
|
|
277
|
-
data = @session_persistence.load_session(session_id)
|
|
278
|
-
if data
|
|
279
|
-
@conversation.replace!(data[:messages])
|
|
280
|
-
@session_id = session_id
|
|
281
|
-
@renderer.info("Resumed session #{session_id[0..7]}")
|
|
282
|
-
else
|
|
283
|
-
@renderer.error("Session not found: #{session_id}")
|
|
284
|
-
end
|
|
285
|
-
else
|
|
286
|
-
sessions = @session_persistence.list_sessions(project_path: @project_root, limit: 10)
|
|
287
|
-
if sessions.empty?
|
|
288
|
-
@renderer.info("No previous sessions.")
|
|
289
|
-
else
|
|
290
|
-
sessions.each do |s|
|
|
291
|
-
puts " #{s[:id][0..7]} | #{s[:title] || 'untitled'} | #{s[:created_at]}"
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def handle_review(args)
|
|
298
|
-
base = args[0] || "main"
|
|
299
|
-
focus = args[1] || "all"
|
|
300
|
-
handle_message("Use the review_pr tool to review my current branch against #{base}. Focus: #{focus}. Load relevant best practice skills for any issues you find.")
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def handle_spawn_teammate(args)
|
|
304
|
-
name = args[0]
|
|
305
|
-
unless name
|
|
306
|
-
@renderer.error("Usage: /spawn <name> [role]")
|
|
307
|
-
return
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
role = args[1] || "coder"
|
|
290
|
+
# ── Teammate Handling ────────────────────────────────────────────
|
|
311
291
|
|
|
292
|
+
def spawn_teammate(name, role)
|
|
312
293
|
mailbox = Teams::Mailbox.new(@db)
|
|
313
294
|
manager = Teams::Manager.new(@db, mailbox: mailbox)
|
|
314
295
|
teammate = manager.spawn(name: name, role: role)
|
|
@@ -350,62 +331,46 @@ module RubynCode
|
|
|
350
331
|
sleep 5
|
|
351
332
|
end
|
|
352
333
|
rescue StandardError => e
|
|
353
|
-
|
|
334
|
+
RubynCode::Debug.agent("Teammate #{teammate.name} error: #{e.message}")
|
|
354
335
|
end
|
|
355
336
|
|
|
337
|
+
# ── Display ──────────────────────────────────────────────────────
|
|
338
|
+
|
|
356
339
|
def display_commands
|
|
357
|
-
@renderer.info(
|
|
358
|
-
|
|
359
|
-
|
|
340
|
+
@renderer.info('Available commands:')
|
|
341
|
+
@command_registry.visible_commands.each do |cmd_class|
|
|
342
|
+
names = cmd_class.all_names.join(', ')
|
|
343
|
+
puts " #{names.ljust(25)} #{cmd_class.description}"
|
|
360
344
|
end
|
|
361
|
-
puts
|
|
345
|
+
puts
|
|
362
346
|
end
|
|
363
347
|
|
|
364
|
-
|
|
365
|
-
puts <<~HELP
|
|
366
|
-
Commands:
|
|
367
|
-
/help Show this help message
|
|
368
|
-
/quit Exit Rubyn Code
|
|
369
|
-
/compact Compress conversation context
|
|
370
|
-
/cost Show token usage and costs
|
|
371
|
-
/clear Clear the terminal
|
|
372
|
-
/undo Remove last exchange
|
|
373
|
-
/tasks List all tasks
|
|
374
|
-
/budget [amt] Show or set session budget
|
|
375
|
-
/skill [name] Load a skill or list available skills
|
|
376
|
-
/resume [id] Resume a session or list recent sessions
|
|
377
|
-
/version Show version
|
|
378
|
-
|
|
379
|
-
Tips:
|
|
380
|
-
- Use @filename to include file contents in your message
|
|
381
|
-
- End a line with \\ for multiline input
|
|
382
|
-
HELP
|
|
383
|
-
end
|
|
348
|
+
# ── Readline ─────────────────────────────────────────────────────
|
|
384
349
|
|
|
385
350
|
def setup_readline!
|
|
386
|
-
|
|
351
|
+
completions = @command_registry.completions
|
|
387
352
|
|
|
388
|
-
|
|
389
|
-
if input.start_with?(
|
|
390
|
-
|
|
353
|
+
Reline.completion_proc = proc do |input|
|
|
354
|
+
if input.start_with?('/')
|
|
355
|
+
completions.select { |c| c.start_with?(input) }
|
|
391
356
|
else
|
|
392
357
|
[]
|
|
393
358
|
end
|
|
394
359
|
end
|
|
395
|
-
|
|
360
|
+
Reline.completion_append_character = ' '
|
|
396
361
|
end
|
|
397
362
|
|
|
398
363
|
def read_input
|
|
399
364
|
lines = []
|
|
400
|
-
prompt_str = lines.empty? ? @renderer.prompt :
|
|
365
|
+
prompt_str = lines.empty? ? @renderer.prompt : ' ... '
|
|
401
366
|
|
|
402
367
|
loop do
|
|
403
|
-
line =
|
|
368
|
+
line = Reline.readline(prompt_str, true)
|
|
404
369
|
return nil if line.nil?
|
|
405
370
|
|
|
406
371
|
if @input_handler.multiline?(line)
|
|
407
372
|
lines << @input_handler.strip_continuation(line)
|
|
408
|
-
prompt_str =
|
|
373
|
+
prompt_str = ' ... '
|
|
409
374
|
else
|
|
410
375
|
lines << line
|
|
411
376
|
break
|
|
@@ -415,9 +380,11 @@ module RubynCode
|
|
|
415
380
|
lines.join("\n")
|
|
416
381
|
end
|
|
417
382
|
|
|
383
|
+
# ── Utilities ────────────────────────────────────────────────────
|
|
384
|
+
|
|
418
385
|
def ensure_home_dir!
|
|
419
386
|
dir = Config::Defaults::HOME_DIR
|
|
420
|
-
FileUtils.mkdir_p(dir)
|
|
387
|
+
FileUtils.mkdir_p(dir)
|
|
421
388
|
end
|
|
422
389
|
|
|
423
390
|
def ensure_auth!
|
|
@@ -428,25 +395,25 @@ module RubynCode
|
|
|
428
395
|
return true
|
|
429
396
|
end
|
|
430
397
|
|
|
431
|
-
@renderer.error(
|
|
432
|
-
@renderer.info(
|
|
433
|
-
@renderer.info(
|
|
434
|
-
@renderer.info(
|
|
398
|
+
@renderer.error('No valid authentication found.')
|
|
399
|
+
@renderer.info('Options:')
|
|
400
|
+
@renderer.info(' 1. Run Claude Code once to authenticate (Rubyn Code reads the keychain token)')
|
|
401
|
+
@renderer.info(' 2. Set ANTHROPIC_API_KEY environment variable')
|
|
435
402
|
@renderer.info(" 3. Run 'rubyn-code --auth' to enter an API key")
|
|
436
403
|
exit(1)
|
|
437
404
|
end
|
|
438
405
|
|
|
439
406
|
def skill_dirs
|
|
440
|
-
dirs = [File.expand_path(
|
|
441
|
-
project_skills = File.join(@project_root,
|
|
407
|
+
dirs = [File.expand_path('../../../skills', __dir__)]
|
|
408
|
+
project_skills = File.join(@project_root, '.rubyn-code', 'skills')
|
|
442
409
|
dirs << project_skills if Dir.exist?(project_skills)
|
|
443
|
-
user_skills = File.join(Config::Defaults::HOME_DIR,
|
|
410
|
+
user_skills = File.join(Config::Defaults::HOME_DIR, 'skills')
|
|
444
411
|
dirs << user_skills if Dir.exist?(user_skills)
|
|
445
412
|
dirs
|
|
446
413
|
end
|
|
447
414
|
|
|
448
415
|
def current_session_id
|
|
449
|
-
@
|
|
416
|
+
@current_session_id ||= SecureRandom.hex(16)
|
|
450
417
|
end
|
|
451
418
|
|
|
452
419
|
def save_session!
|
|
@@ -467,14 +434,14 @@ module RubynCode
|
|
|
467
434
|
end
|
|
468
435
|
|
|
469
436
|
GOODBYE_MESSAGES = [
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
"GC.start on this session... Stay Ruby, friend!
|
|
437
|
+
'Freezing strings and saving memories... See ya! 💎',
|
|
438
|
+
'Memoizing this session... Until next time! 🧠',
|
|
439
|
+
'Committing learnings to memory... Later! 🤙',
|
|
440
|
+
'Saving state, yielding control... Bye for now! 👋',
|
|
441
|
+
'Session.save! && Rubyn.sleep... Catch you later! 😴',
|
|
442
|
+
"GC.start on this session... Stay Ruby, friend! ✌\uFE0F",
|
|
476
443
|
"Writing instincts to disk... Don't forget me! 💾",
|
|
477
|
-
"at_exit { puts 'Thanks for coding with Rubyn!' } 🎸"
|
|
444
|
+
"at_exit { puts 'Thanks for coding with Rubyn!' } 🎸"
|
|
478
445
|
].freeze
|
|
479
446
|
|
|
480
447
|
def shutdown!
|
|
@@ -485,21 +452,21 @@ module RubynCode
|
|
|
485
452
|
puts
|
|
486
453
|
@renderer.info(GOODBYE_MESSAGES.sample)
|
|
487
454
|
|
|
488
|
-
@renderer.info(
|
|
455
|
+
@renderer.info('Saving session...')
|
|
489
456
|
save_session!
|
|
490
457
|
@background_worker&.shutdown!
|
|
491
458
|
|
|
492
459
|
if @conversation.length > 5
|
|
493
460
|
begin
|
|
494
|
-
@renderer.info(
|
|
461
|
+
@renderer.info('Extracting learnings from this session...')
|
|
495
462
|
Learning::Extractor.call(
|
|
496
463
|
@conversation.messages,
|
|
497
464
|
llm_client: @llm_client,
|
|
498
465
|
project_path: @project_root
|
|
499
466
|
)
|
|
500
|
-
@renderer.success(
|
|
467
|
+
@renderer.success('Instincts saved.')
|
|
501
468
|
rescue StandardError => e
|
|
502
|
-
|
|
469
|
+
RubynCode::Debug.warn("Instinct extraction skipped: #{e.message}")
|
|
503
470
|
end
|
|
504
471
|
end
|
|
505
472
|
|
|
@@ -510,7 +477,7 @@ module RubynCode
|
|
|
510
477
|
# Silent — decay is best-effort
|
|
511
478
|
end
|
|
512
479
|
|
|
513
|
-
@renderer.info("Session saved. Rubyn out.
|
|
480
|
+
@renderer.info("Session saved. Rubyn out. ✌\uFE0F")
|
|
514
481
|
rescue StandardError
|
|
515
482
|
# Best effort on shutdown
|
|
516
483
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module RubynCode
|
|
6
|
+
module CLI
|
|
7
|
+
# Installs a pinned launcher that bypasses rbenv/rvm version switching.
|
|
8
|
+
#
|
|
9
|
+
# When rubyn-code is installed as a gem, RubyGems generates a wrapper in
|
|
10
|
+
# the Ruby version's bin dir (e.g. ~/.rbenv/versions/3.4.5/bin/rubyn-code)
|
|
11
|
+
# with a shebang pointing to the correct Ruby. However, rbenv's shim at
|
|
12
|
+
# ~/.rbenv/shims/rubyn-code intercepts the command and re-resolves Ruby
|
|
13
|
+
# based on .ruby-version in the current directory — which breaks rubyn-code
|
|
14
|
+
# when you cd into a project using a different Ruby.
|
|
15
|
+
#
|
|
16
|
+
# This setup creates a launcher in ~/.local/bin (which typically appears
|
|
17
|
+
# before ~/.rbenv/shims in PATH) that calls the gem wrapper directly,
|
|
18
|
+
# bypassing the shim entirely.
|
|
19
|
+
class Setup
|
|
20
|
+
INSTALL_DIR = File.expand_path('~/.local/bin')
|
|
21
|
+
LAUNCHER_NAME = 'rubyn-code'
|
|
22
|
+
|
|
23
|
+
def self.run
|
|
24
|
+
new.run
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
gem_wrapper = find_gem_wrapper
|
|
29
|
+
unless gem_wrapper
|
|
30
|
+
warn 'Error: Could not find the rubyn-code gem wrapper.'
|
|
31
|
+
warn 'Make sure rubyn-code is installed: gem install rubyn-code'
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
pinned_ruby = extract_ruby_from_wrapper(gem_wrapper)
|
|
36
|
+
unless pinned_ruby
|
|
37
|
+
warn "Error: Could not determine Ruby path from #{gem_wrapper}"
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
launcher_path = File.join(INSTALL_DIR, LAUNCHER_NAME)
|
|
42
|
+
|
|
43
|
+
FileUtils.mkdir_p(INSTALL_DIR)
|
|
44
|
+
write_launcher(launcher_path, gem_wrapper, pinned_ruby)
|
|
45
|
+
File.chmod(0o755, launcher_path)
|
|
46
|
+
|
|
47
|
+
puts "Installed pinned launcher to #{launcher_path}"
|
|
48
|
+
puts " Ruby: #{pinned_ruby}"
|
|
49
|
+
puts " Wrapper: #{gem_wrapper}"
|
|
50
|
+
puts
|
|
51
|
+
|
|
52
|
+
verify_path(launcher_path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def find_gem_wrapper
|
|
58
|
+
# The gem wrapper lives in the same bin dir as the current Ruby
|
|
59
|
+
ruby_bin = RbConfig::CONFIG['bindir']
|
|
60
|
+
candidate = File.join(ruby_bin, LAUNCHER_NAME)
|
|
61
|
+
return candidate if File.exist?(candidate)
|
|
62
|
+
|
|
63
|
+
# Fallback: search gem paths
|
|
64
|
+
Gem.path.each do |gem_path|
|
|
65
|
+
bin = File.join(gem_path, 'bin', LAUNCHER_NAME)
|
|
66
|
+
return bin if File.exist?(bin)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def extract_ruby_from_wrapper(wrapper_path)
|
|
73
|
+
first_line = File.open(wrapper_path, &:readline).strip
|
|
74
|
+
if first_line.start_with?('#!')
|
|
75
|
+
ruby_path = first_line.sub(/\A#!\s*/, '')
|
|
76
|
+
return ruby_path if File.executable?(ruby_path)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Fallback to the Ruby running this process
|
|
80
|
+
RbConfig.ruby
|
|
81
|
+
rescue EOFError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def write_launcher(path, gem_wrapper, pinned_ruby)
|
|
86
|
+
File.write(path, <<~BASH)
|
|
87
|
+
#!/usr/bin/env bash
|
|
88
|
+
# Rubyn Code launcher — pinned to Ruby #{File.basename(File.dirname(pinned_ruby, 2))}
|
|
89
|
+
# Generated by: rubyn-code --setup
|
|
90
|
+
# Bypasses rbenv/rvm so rubyn-code works in any project.
|
|
91
|
+
#
|
|
92
|
+
# To regenerate: rubyn-code --setup
|
|
93
|
+
# To remove: rm #{path}
|
|
94
|
+
exec "#{gem_wrapper}" "$@"
|
|
95
|
+
BASH
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def verify_path(launcher_path)
|
|
99
|
+
install_dir = File.dirname(launcher_path)
|
|
100
|
+
path_dirs = ENV['PATH'].split(File::PATH_SEPARATOR)
|
|
101
|
+
shim_index = path_dirs.index { |d| d.include?('rbenv/shims') || d.include?('rvm') }
|
|
102
|
+
install_index = path_dirs.index(install_dir)
|
|
103
|
+
|
|
104
|
+
if install_index.nil?
|
|
105
|
+
puts "Warning: #{install_dir} is not in your PATH."
|
|
106
|
+
puts 'Add this to your shell profile:'
|
|
107
|
+
puts " export PATH=\"#{install_dir}:$PATH\""
|
|
108
|
+
elsif shim_index && install_index > shim_index
|
|
109
|
+
puts "Warning: #{install_dir} comes AFTER rbenv shims in PATH."
|
|
110
|
+
puts 'Move it before the shims in your shell profile so it takes priority.'
|
|
111
|
+
else
|
|
112
|
+
puts "PATH looks good — #{install_dir} takes priority over version manager shims."
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|