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.
Files changed (154) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +151 -5
  3. data/db/migrations/013_add_failed_status_to_tasks.rb +51 -0
  4. data/lib/rubyn_code/agent/background_job_handler.rb +71 -0
  5. data/lib/rubyn_code/agent/conversation.rb +84 -56
  6. data/lib/rubyn_code/agent/dynamic_tool_schema.rb +152 -0
  7. data/lib/rubyn_code/agent/feedback_handler.rb +49 -0
  8. data/lib/rubyn_code/agent/llm_caller.rb +157 -0
  9. data/lib/rubyn_code/agent/loop.rb +182 -683
  10. data/lib/rubyn_code/agent/loop_detector.rb +50 -11
  11. data/lib/rubyn_code/agent/prompts.rb +109 -0
  12. data/lib/rubyn_code/agent/response_modes.rb +111 -0
  13. data/lib/rubyn_code/agent/response_parser.rb +111 -0
  14. data/lib/rubyn_code/agent/system_prompt_builder.rb +211 -0
  15. data/lib/rubyn_code/agent/tool_processor.rb +178 -0
  16. data/lib/rubyn_code/agent/usage_tracker.rb +59 -0
  17. data/lib/rubyn_code/auth/key_encryption.rb +118 -0
  18. data/lib/rubyn_code/auth/oauth.rb +80 -64
  19. data/lib/rubyn_code/auth/server.rb +21 -24
  20. data/lib/rubyn_code/auth/token_store.rb +80 -52
  21. data/lib/rubyn_code/autonomous/daemon.rb +146 -32
  22. data/lib/rubyn_code/autonomous/idle_poller.rb +4 -24
  23. data/lib/rubyn_code/autonomous/task_claimer.rb +46 -44
  24. data/lib/rubyn_code/background/worker.rb +64 -76
  25. data/lib/rubyn_code/cli/app.rb +159 -114
  26. data/lib/rubyn_code/cli/commands/doctor.rb +73 -0
  27. data/lib/rubyn_code/cli/commands/mcp.rb +77 -0
  28. data/lib/rubyn_code/cli/commands/model.rb +105 -18
  29. data/lib/rubyn_code/cli/commands/new_session.rb +45 -0
  30. data/lib/rubyn_code/cli/commands/provider.rb +123 -0
  31. data/lib/rubyn_code/cli/commands/skill.rb +52 -3
  32. data/lib/rubyn_code/cli/daemon_runner.rb +64 -11
  33. data/lib/rubyn_code/cli/first_run.rb +159 -0
  34. data/lib/rubyn_code/cli/renderer.rb +109 -60
  35. data/lib/rubyn_code/cli/repl.rb +48 -374
  36. data/lib/rubyn_code/cli/repl_commands.rb +177 -0
  37. data/lib/rubyn_code/cli/repl_lifecycle.rb +76 -0
  38. data/lib/rubyn_code/cli/repl_setup.rb +181 -0
  39. data/lib/rubyn_code/cli/setup.rb +6 -2
  40. data/lib/rubyn_code/cli/stream_formatter.rb +56 -49
  41. data/lib/rubyn_code/cli/version_check.rb +28 -11
  42. data/lib/rubyn_code/config/defaults.rb +11 -0
  43. data/lib/rubyn_code/config/project_profile.rb +185 -0
  44. data/lib/rubyn_code/config/schema.json +49 -0
  45. data/lib/rubyn_code/config/settings.rb +103 -1
  46. data/lib/rubyn_code/config/validator.rb +63 -0
  47. data/lib/rubyn_code/context/auto_compact.rb +1 -1
  48. data/lib/rubyn_code/context/context_budget.rb +182 -0
  49. data/lib/rubyn_code/context/context_collapse.rb +34 -4
  50. data/lib/rubyn_code/context/decision_compactor.rb +99 -0
  51. data/lib/rubyn_code/context/manager.rb +44 -8
  52. data/lib/rubyn_code/context/manual_compact.rb +1 -1
  53. data/lib/rubyn_code/context/micro_compact.rb +29 -19
  54. data/lib/rubyn_code/context/schema_filter.rb +64 -0
  55. data/lib/rubyn_code/db/connection.rb +31 -26
  56. data/lib/rubyn_code/db/migrator.rb +44 -28
  57. data/lib/rubyn_code/hooks/built_in.rb +14 -10
  58. data/lib/rubyn_code/hooks/registry.rb +4 -0
  59. data/lib/rubyn_code/ide/adapters/tool_output.rb +330 -0
  60. data/lib/rubyn_code/ide/client.rb +110 -0
  61. data/lib/rubyn_code/ide/handlers/accept_edit_handler.rb +35 -0
  62. data/lib/rubyn_code/ide/handlers/approve_tool_use_handler.rb +34 -0
  63. data/lib/rubyn_code/ide/handlers/cancel_handler.rb +41 -0
  64. data/lib/rubyn_code/ide/handlers/config_get_handler.rb +63 -0
  65. data/lib/rubyn_code/ide/handlers/config_set_handler.rb +86 -0
  66. data/lib/rubyn_code/ide/handlers/initialize_handler.rb +79 -0
  67. data/lib/rubyn_code/ide/handlers/models_list_handler.rb +39 -0
  68. data/lib/rubyn_code/ide/handlers/prompt_handler.rb +215 -0
  69. data/lib/rubyn_code/ide/handlers/review_handler.rb +110 -0
  70. data/lib/rubyn_code/ide/handlers/session_fork_handler.rb +49 -0
  71. data/lib/rubyn_code/ide/handlers/session_list_handler.rb +41 -0
  72. data/lib/rubyn_code/ide/handlers/session_reset_handler.rb +31 -0
  73. data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +42 -0
  74. data/lib/rubyn_code/ide/handlers/shutdown_handler.rb +37 -0
  75. data/lib/rubyn_code/ide/handlers.rb +76 -0
  76. data/lib/rubyn_code/ide/protocol.rb +111 -0
  77. data/lib/rubyn_code/ide/server.rb +186 -0
  78. data/lib/rubyn_code/index/codebase_index.rb +311 -0
  79. data/lib/rubyn_code/learning/extractor.rb +65 -82
  80. data/lib/rubyn_code/learning/injector.rb +22 -23
  81. data/lib/rubyn_code/learning/instinct.rb +71 -42
  82. data/lib/rubyn_code/learning/shortcut.rb +95 -0
  83. data/lib/rubyn_code/llm/adapters/anthropic.rb +274 -0
  84. data/lib/rubyn_code/llm/adapters/anthropic_compatible.rb +60 -0
  85. data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +215 -0
  86. data/lib/rubyn_code/llm/adapters/base.rb +35 -0
  87. data/lib/rubyn_code/llm/adapters/json_parsing.rb +21 -0
  88. data/lib/rubyn_code/llm/adapters/openai.rb +246 -0
  89. data/lib/rubyn_code/llm/adapters/openai_compatible.rb +50 -0
  90. data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +90 -0
  91. data/lib/rubyn_code/llm/adapters/openai_streaming.rb +141 -0
  92. data/lib/rubyn_code/llm/adapters/prompt_caching.rb +60 -0
  93. data/lib/rubyn_code/llm/client.rb +75 -247
  94. data/lib/rubyn_code/llm/model_router.rb +237 -0
  95. data/lib/rubyn_code/llm/streaming.rb +4 -227
  96. data/lib/rubyn_code/mcp/client.rb +1 -1
  97. data/lib/rubyn_code/mcp/config.rb +10 -12
  98. data/lib/rubyn_code/mcp/sse_transport.rb +15 -13
  99. data/lib/rubyn_code/mcp/stdio_transport.rb +16 -18
  100. data/lib/rubyn_code/mcp/tool_bridge.rb +31 -62
  101. data/lib/rubyn_code/memory/search.rb +1 -0
  102. data/lib/rubyn_code/memory/session_persistence.rb +59 -58
  103. data/lib/rubyn_code/memory/store.rb +42 -55
  104. data/lib/rubyn_code/observability/budget_enforcer.rb +46 -32
  105. data/lib/rubyn_code/observability/cost_calculator.rb +32 -8
  106. data/lib/rubyn_code/observability/skill_analytics.rb +116 -0
  107. data/lib/rubyn_code/observability/token_analytics.rb +130 -0
  108. data/lib/rubyn_code/observability/usage_reporter.rb +79 -61
  109. data/lib/rubyn_code/output/diff_renderer.rb +102 -77
  110. data/lib/rubyn_code/output/formatter.rb +11 -11
  111. data/lib/rubyn_code/permissions/policy.rb +11 -13
  112. data/lib/rubyn_code/permissions/prompter.rb +8 -9
  113. data/lib/rubyn_code/protocols/plan_approval.rb +25 -20
  114. data/lib/rubyn_code/self_test.rb +315 -0
  115. data/lib/rubyn_code/skills/catalog.rb +66 -0
  116. data/lib/rubyn_code/skills/document.rb +33 -29
  117. data/lib/rubyn_code/skills/loader.rb +43 -0
  118. data/lib/rubyn_code/skills/ttl_manager.rb +100 -0
  119. data/lib/rubyn_code/sub_agents/runner.rb +20 -25
  120. data/lib/rubyn_code/tasks/dag.rb +25 -24
  121. data/lib/rubyn_code/tasks/models.rb +1 -0
  122. data/lib/rubyn_code/tools/ask_user.rb +44 -0
  123. data/lib/rubyn_code/tools/background_run.rb +2 -1
  124. data/lib/rubyn_code/tools/base.rb +39 -32
  125. data/lib/rubyn_code/tools/bash.rb +7 -1
  126. data/lib/rubyn_code/tools/edit_file.rb +130 -17
  127. data/lib/rubyn_code/tools/executor.rb +130 -25
  128. data/lib/rubyn_code/tools/file_cache.rb +95 -0
  129. data/lib/rubyn_code/tools/git_commit.rb +12 -10
  130. data/lib/rubyn_code/tools/git_log.rb +12 -10
  131. data/lib/rubyn_code/tools/glob.rb +29 -7
  132. data/lib/rubyn_code/tools/grep.rb +8 -1
  133. data/lib/rubyn_code/tools/ide_diagnostics.rb +51 -0
  134. data/lib/rubyn_code/tools/ide_symbols.rb +53 -0
  135. data/lib/rubyn_code/tools/load_skill.rb +13 -6
  136. data/lib/rubyn_code/tools/memory_search.rb +14 -13
  137. data/lib/rubyn_code/tools/memory_write.rb +2 -1
  138. data/lib/rubyn_code/tools/output_compressor.rb +190 -0
  139. data/lib/rubyn_code/tools/read_file.rb +17 -6
  140. data/lib/rubyn_code/tools/registry.rb +11 -0
  141. data/lib/rubyn_code/tools/review_pr.rb +127 -80
  142. data/lib/rubyn_code/tools/run_specs.rb +26 -15
  143. data/lib/rubyn_code/tools/schema.rb +4 -10
  144. data/lib/rubyn_code/tools/spawn_agent.rb +113 -82
  145. data/lib/rubyn_code/tools/spawn_teammate.rb +107 -64
  146. data/lib/rubyn_code/tools/spec_output_parser.rb +118 -0
  147. data/lib/rubyn_code/tools/task.rb +17 -17
  148. data/lib/rubyn_code/tools/web_fetch.rb +62 -47
  149. data/lib/rubyn_code/tools/web_search.rb +66 -48
  150. data/lib/rubyn_code/tools/write_file.rb +76 -1
  151. data/lib/rubyn_code/version.rb +1 -1
  152. data/lib/rubyn_code.rb +62 -1
  153. data/skills/rubyn_self_test.md +133 -0
  154. metadata +83 -1
@@ -33,14 +33,23 @@ module RubynCode
33
33
  puts @pastel.cyan(" > #{name}: #{format_params(params)}")
34
34
  end
35
35
 
36
- def tool_result(_name, output)
37
- truncated = output.to_s[0...300]
38
- lines = truncated.lines
39
- if lines.length > 6
40
- puts @pastel.dim(" #{lines[0..4].map(&:strip).join("\n ")}")
41
- puts @pastel.dim(" ... (#{lines.length - 5} more lines)")
36
+ DIFF_TOOLS = %w[edit_file write_file].freeze
37
+ DIFF_RESULT_LIMIT = 2000
38
+ DEFAULT_RESULT_LIMIT = 500
39
+
40
+ def tool_result(name, output)
41
+ raw = output.to_s
42
+
43
+ if DIFF_TOOLS.include?(name.to_s)
44
+ render_diff_result(raw[0...DIFF_RESULT_LIMIT].lines)
42
45
  else
43
- puts @pastel.dim(" #{truncated.strip.gsub("\n", "\n ")}")
46
+ truncated = raw[0...DEFAULT_RESULT_LIMIT]
47
+ lines = truncated.lines
48
+ if lines.length > 6
49
+ render_truncated_result(lines)
50
+ else
51
+ puts @pastel.dim(" #{truncated.strip.gsub("\n", "\n ")}")
52
+ end
44
53
  end
45
54
  end
46
55
 
@@ -80,54 +89,93 @@ module RubynCode
80
89
 
81
90
  def cost_summary(session_cost:, daily_cost:, tokens:)
82
91
  puts @pastel.bold('Cost Summary:')
83
- puts " Session: $#{'%.4f' % session_cost}"
84
- puts " Today: $#{'%.4f' % daily_cost}"
92
+ puts format(' Session: $%.4f', session_cost)
93
+ puts format(' Today: $%.4f', daily_cost)
85
94
  puts " Tokens: #{tokens[:input]} in / #{tokens[:output]} out"
86
95
  end
87
96
 
88
97
  def prompt
89
- if @yolo
90
- @pastel.bold.green('rubyn ') + @pastel.bold.red('YOLO') + @pastel.bold.green(' > ')
91
- else
92
- @pastel.bold.green('rubyn > ')
93
- end
98
+ return @pastel.bold.green('rubyn ') + @pastel.bold.red('YOLO') + @pastel.bold.green(' > ') if @yolo
99
+
100
+ @pastel.bold.green('rubyn > ')
94
101
  end
95
102
 
103
+ DIFF_COLORS = {
104
+ /\A \+ / => :green,
105
+ /\A - / => :red,
106
+ /\A @@ / => :cyan,
107
+ /\A(?:Created|Updated|Edited) / => :yellow
108
+ }.freeze
109
+
96
110
  private
97
111
 
112
+ def render_diff_result(lines)
113
+ lines.each do |line|
114
+ stripped = line.rstrip
115
+ puts " #{colorize_diff_line(stripped)}"
116
+ end
117
+ end
118
+
119
+ def colorize_diff_line(line)
120
+ DIFF_COLORS.each do |pattern, color|
121
+ return @pastel.decorate(line, color) if line.match?(pattern)
122
+ end
123
+ @pastel.dim(line)
124
+ end
125
+
126
+ def render_truncated_result(lines)
127
+ puts @pastel.dim(" #{lines[0..4].map(&:strip).join("\n ")}")
128
+ puts @pastel.dim(" ... (#{lines.length - 5} more lines)")
129
+ end
130
+
98
131
  def render_markdown(text)
99
132
  lines = text.lines
100
133
  result = []
101
-
102
134
  in_code_block = false
103
135
  code_lang = nil
104
136
  code_buffer = []
105
137
 
106
138
  lines.each do |line|
107
- if line.strip.match?(/\A```(\w*)/)
108
- if in_code_block
109
- result << render_code_block(code_buffer.join, code_lang)
110
- code_buffer = []
111
- in_code_block = false
112
- code_lang = nil
113
- else
114
- in_code_block = true
115
- code_lang = line.strip.match(/\A```(\w*)/)[1]
116
- code_lang = 'ruby' if code_lang.empty?
117
- end
118
- elsif in_code_block
119
- code_buffer << line
120
- else
121
- result << render_line(line)
122
- end
139
+ in_code_block, code_lang, code_buffer = process_markdown_line(
140
+ line, in_code_block, code_lang, code_buffer, result
141
+ )
123
142
  end
124
143
 
125
144
  # Flush any unclosed code block
126
- result << render_code_block(code_buffer.join, code_lang || 'ruby') unless code_buffer.empty?
145
+ flush_code_buffer(code_buffer, code_lang, result)
127
146
 
128
147
  result.join
129
148
  end
130
149
 
150
+ def process_markdown_line(line, in_code_block, code_lang, code_buffer, result)
151
+ if line.strip.match?(/\A```(\w*)/)
152
+ handle_code_fence(line, in_code_block, code_lang, code_buffer, result)
153
+ elsif in_code_block
154
+ code_buffer << line
155
+ [in_code_block, code_lang, code_buffer]
156
+ else
157
+ result << render_line(line)
158
+ [in_code_block, code_lang, code_buffer]
159
+ end
160
+ end
161
+
162
+ def handle_code_fence(line, in_code_block, code_lang, code_buffer, result)
163
+ if in_code_block
164
+ result << render_code_block(code_buffer.join, code_lang)
165
+ [false, nil, []]
166
+ else
167
+ lang = line.strip.match(/\A```(\w*)/)[1]
168
+ lang = 'ruby' if lang.empty?
169
+ [true, lang, []]
170
+ end
171
+ end
172
+
173
+ def flush_code_buffer(code_buffer, code_lang, result)
174
+ return if code_buffer.empty?
175
+
176
+ result << render_code_block(code_buffer.join, code_lang || 'ruby')
177
+ end
178
+
131
179
  def render_code_block(code, lang)
132
180
  lexer = Rouge::Lexer.find(lang) || Rouge::Lexers::PlainText.new
133
181
  highlighted = @rouge_formatter.format(lexer.lex(code))
@@ -139,37 +187,38 @@ module RubynCode
139
187
  end
140
188
 
141
189
  def render_line(line)
142
- # Headers
143
- if line.match?(/\A\s*\#{1,6}\s/)
144
- level = line.match(/\A\s*(\#{1,6})\s/)[1].length
145
- text = line.sub(/\A\s*\#{1,6}\s+/, '').rstrip
146
- case level
147
- when 1 then "#{@pastel.bold.underline(text)}\n"
148
- when 2 then "#{@pastel.bold(text)}\n"
149
- else "#{@pastel.bold(text)}\n"
150
- end
151
- # Bullet lists
152
- elsif line.match?(/\A\s*[-*]\s/)
153
- indent = line.match(/\A(\s*)/)[1]
154
- content = line.sub(/\A\s*[-*]\s+/, '')
155
- "#{indent} #{@pastel.cyan('•')} #{render_inline(content)}"
156
- # Numbered lists
157
- elsif line.match?(/\A\s*\d+\.\s/)
158
- indent = line.match(/\A(\s*)/)[1]
159
- num = line.match(/(\d+)\./)[1]
160
- content = line.sub(/\A\s*\d+\.\s+/, '')
161
- "#{indent} #{@pastel.cyan("#{num}.")} #{render_inline(content)}"
162
- # Horizontal rules
163
- elsif line.strip.match?(/\A-{3,}\z/)
164
- "#{@pastel.dim('─' * [terminal_width - 4, 40].min)}\n"
165
- # Table rows
166
- elsif line.include?('|')
167
- render_table_row(line)
168
- else
169
- render_inline(line)
190
+ case line
191
+ when /\A\s*\#{1,6}\s/ then render_header(line)
192
+ when /\A\s*[-*]\s/ then render_bullet(line)
193
+ when /\A\s*\d+\.\s/ then render_numbered_item(line)
194
+ when ->(l) { l.strip.match?(/\A-{3,}\z/) } then "#{@pastel.dim('─' * [terminal_width - 4, 40].min)}\n"
195
+ when /\|/ then render_table_row(line)
196
+ else render_inline(line)
197
+ end
198
+ end
199
+
200
+ def render_header(line)
201
+ level = line.match(/\A\s*(\#{1,6})\s/)[1].length
202
+ text = line.sub(/\A\s*\#{1,6}\s+/, '').rstrip
203
+ case level
204
+ when 1 then "#{@pastel.bold.underline(text)}\n"
205
+ else "#{@pastel.bold(text)}\n"
170
206
  end
171
207
  end
172
208
 
209
+ def render_bullet(line)
210
+ indent = line.match(/\A(\s*)/)[1]
211
+ content = line.sub(/\A\s*[-*]\s+/, '')
212
+ "#{indent} #{@pastel.cyan('•')} #{render_inline(content)}"
213
+ end
214
+
215
+ def render_numbered_item(line)
216
+ indent = line.match(/\A(\s*)/)[1]
217
+ num = line.match(/(\d+)\./)[1]
218
+ content = line.sub(/\A\s*\d+\.\s+/, '')
219
+ "#{indent} #{@pastel.cyan("#{num}.")} #{render_inline(content)}"
220
+ end
221
+
173
222
  def render_inline(text)
174
223
  text
175
224
  .gsub(/\*\*(.+?)\*\*/) { @pastel.bold(Regexp.last_match(1)) }