rubycode 0.1.2 → 0.1.3

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/README.md +33 -4
  4. data/config/locales/en.yml +87 -0
  5. data/config/system_prompt.md +54 -0
  6. data/config/tools/done.json +2 -2
  7. data/config/tools/update.json +25 -0
  8. data/config/tools/write.json +21 -0
  9. data/docs/images/demo.png +0 -0
  10. data/lib/rubycode/adapters/ollama.rb +76 -3
  11. data/lib/rubycode/agent_loop.rb +41 -16
  12. data/lib/rubycode/client/approval_handler.rb +70 -0
  13. data/lib/rubycode/client/display_formatter.rb +32 -12
  14. data/lib/rubycode/client/response_handler.rb +20 -12
  15. data/lib/rubycode/client.rb +25 -36
  16. data/lib/rubycode/configuration.rb +8 -1
  17. data/lib/rubycode/database.rb +50 -0
  18. data/lib/rubycode/errors.rb +12 -0
  19. data/lib/rubycode/models/base.rb +68 -0
  20. data/lib/rubycode/models/memory.rb +57 -0
  21. data/lib/rubycode/models.rb +4 -0
  22. data/lib/rubycode/tools/base.rb +1 -10
  23. data/lib/rubycode/tools/bash.rb +10 -7
  24. data/lib/rubycode/tools/read.rb +3 -0
  25. data/lib/rubycode/tools/update.rb +80 -0
  26. data/lib/rubycode/tools/write.rb +57 -0
  27. data/lib/rubycode/tools.rb +4 -0
  28. data/lib/rubycode/version.rb +1 -1
  29. data/lib/rubycode/views/agent_loop/adapter_error.rb +14 -0
  30. data/lib/rubycode/views/agent_loop/iteration_footer.rb +17 -0
  31. data/lib/rubycode/views/agent_loop/iteration_header.rb +24 -0
  32. data/lib/rubycode/views/agent_loop/response_received.rb +17 -0
  33. data/lib/rubycode/views/agent_loop/retry_status.rb +14 -0
  34. data/lib/rubycode/views/agent_loop/thinking_status.rb +17 -0
  35. data/lib/rubycode/views/agent_loop/tool_error.rb +14 -0
  36. data/lib/rubycode/views/agent_loop.rb +8 -0
  37. data/lib/rubycode/views/bash_approval.rb +28 -0
  38. data/lib/rubycode/views/cli/configuration_table.rb +28 -0
  39. data/lib/rubycode/views/cli/error_display.rb +19 -0
  40. data/lib/rubycode/views/cli/error_message.rb +17 -0
  41. data/lib/rubycode/views/cli/exit_message.rb +17 -0
  42. data/lib/rubycode/views/cli/interrupt_message.rb +17 -0
  43. data/lib/rubycode/views/cli/memory_cleared_message.rb +17 -0
  44. data/lib/rubycode/views/cli/ready_message.rb +17 -0
  45. data/lib/rubycode/views/cli/response_box.rb +29 -0
  46. data/lib/rubycode/views/cli.rb +11 -0
  47. data/lib/rubycode/views/formatter/debug_tool_info.rb +17 -0
  48. data/lib/rubycode/views/formatter/info_message.rb +17 -0
  49. data/lib/rubycode/views/formatter/minimal_tool_info.rb +26 -0
  50. data/lib/rubycode/views/formatter/tool_result.rb +20 -0
  51. data/lib/rubycode/views/formatter.rb +7 -0
  52. data/lib/rubycode/views/response_handler/agent_finished.rb +31 -0
  53. data/lib/rubycode/views/response_handler/complete_message.rb +31 -0
  54. data/lib/rubycode/views/response_handler/max_iterations.rb +29 -0
  55. data/lib/rubycode/views/response_handler/max_tool_calls.rb +29 -0
  56. data/lib/rubycode/views/response_handler/tool_injection_warning.rb +17 -0
  57. data/lib/rubycode/views/response_handler.rb +8 -0
  58. data/lib/rubycode/views/skip_notification.rb +15 -0
  59. data/lib/rubycode/views/update_approval.rb +36 -0
  60. data/lib/rubycode/views/welcome.rb +27 -0
  61. data/lib/rubycode/views/write_approval.rb +42 -0
  62. data/lib/rubycode/views.rb +12 -0
  63. data/lib/rubycode.rb +9 -1
  64. data/rubycode_cli.rb +41 -51
  65. metadata +220 -5
  66. data/lib/rubycode/history.rb +0 -39
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module RubyCode
6
+ module Tools
7
+ # Tool for creating new files
8
+ class Write < Base
9
+ private
10
+
11
+ def perform(params)
12
+ file_path = params["file_path"]
13
+ content = params["content"]
14
+ full_path = resolve_path(file_path)
15
+
16
+ validate_file_does_not_exist(file_path, full_path)
17
+ request_approval(file_path, content)
18
+ create_file(full_path, content)
19
+ build_result(file_path, content)
20
+ end
21
+
22
+ def validate_file_does_not_exist(file_path, full_path)
23
+ return unless File.exist?(full_path)
24
+
25
+ raise ToolError, I18n.t("rubycode.errors.file_exists", path: file_path)
26
+ end
27
+
28
+ def request_approval(file_path, content)
29
+ approval_handler = context[:approval_handler]
30
+ return if approval_handler.request_write_approval(file_path, content)
31
+
32
+ raise ToolError, I18n.t("rubycode.errors.user_cancelled_write")
33
+ end
34
+
35
+ def create_file(full_path, content)
36
+ dir = File.dirname(full_path)
37
+ FileUtils.mkdir_p(dir)
38
+ File.write(full_path, content)
39
+ end
40
+
41
+ def build_result(file_path, content)
42
+ ToolResult.new(
43
+ content: "Created '#{file_path}' (#{content.lines.count} lines)",
44
+ metadata: {
45
+ file_path: file_path,
46
+ lines: content.lines.count,
47
+ bytes: content.bytesize
48
+ }
49
+ )
50
+ end
51
+
52
+ def resolve_path(file_path)
53
+ File.absolute_path?(file_path) ? file_path : File.join(root_path, file_path)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -4,6 +4,8 @@ require_relative "tools/base"
4
4
  require_relative "tools/bash"
5
5
  require_relative "tools/read"
6
6
  require_relative "tools/search"
7
+ require_relative "tools/write"
8
+ require_relative "tools/update"
7
9
  require_relative "tools/done"
8
10
 
9
11
  module RubyCode
@@ -14,6 +16,8 @@ module RubyCode
14
16
  Bash,
15
17
  Read,
16
18
  Search,
19
+ Write,
20
+ Update,
17
21
  Done
18
22
  ].freeze
19
23
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyCode
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCode
4
+ module Views
5
+ module AgentLoop
6
+ # Builds adapter error message
7
+ class AdapterError
8
+ def self.build(message:)
9
+ " ✗ Adapter Error: #{message}"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module AgentLoop
8
+ # Builds iteration footer
9
+ class IterationFooter
10
+ def self.build
11
+ pastel = Pastel.new
12
+ pastel.cyan("└────────────────────────────────────────────────")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module AgentLoop
8
+ # Builds iteration header with tool list
9
+ class IterationHeader
10
+ def self.build(iteration:, tool_calls:)
11
+ pastel = Pastel.new
12
+
13
+ header = pastel.cyan("┌─ Iteration #{iteration} ─────────────────────────")
14
+ tool_list = tool_calls.each_with_index.map do |tool_call, idx|
15
+ tool_name = tool_call.dig("function", "name")
16
+ " #{pastel.dim("#{idx + 1}.")} #{tool_name}"
17
+ end
18
+
19
+ [header, *tool_list].join("\n")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module AgentLoop
8
+ # Builds response received status
9
+ class ResponseReceived
10
+ def self.build
11
+ pastel = Pastel.new
12
+ "#{pastel.green("✓")} Response received"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCode
4
+ module Views
5
+ module AgentLoop
6
+ # Builds retry status message
7
+ class RetryStatus
8
+ def self.build(attempt:, max_retries:, delay:, error:)
9
+ "⚠ Request failed: #{error}. Retrying in #{delay.round(1)}s... (attempt #{attempt}/#{max_retries})"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module AgentLoop
8
+ # Builds thinking status indicator
9
+ class ThinkingStatus
10
+ def self.build
11
+ pastel = Pastel.new
12
+ "#{pastel.dim("→")} Thinking..."
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCode
4
+ module Views
5
+ module AgentLoop
6
+ # Builds tool error message
7
+ class ToolError
8
+ def self.build(message:)
9
+ " ✗ #{message}"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Agent loop view components
4
+ require_relative "agent_loop/thinking_status"
5
+ require_relative "agent_loop/response_received"
6
+ require_relative "agent_loop/iteration_header"
7
+ require_relative "agent_loop/iteration_footer"
8
+ require_relative "agent_loop/tool_error"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ # Builds bash command approval prompt display
8
+ class BashApproval
9
+ def self.build(command:, base_command:, safe_commands:)
10
+ pastel = Pastel.new
11
+
12
+ [
13
+ "",
14
+ pastel.red("━" * 80),
15
+ pastel.bold.red("⚠ WARNING: Non-Whitelisted Command"),
16
+ "#{pastel.cyan("Command:")} #{command}",
17
+ "#{pastel.cyan("Base command:")} #{base_command}",
18
+ pastel.red("─" * 80),
19
+ pastel.yellow("This command is not in the safe whitelist:"),
20
+ pastel.dim("Safe commands: #{safe_commands.join(", ")}"),
21
+ "",
22
+ pastel.yellow("⚠ Only approve if you trust this command will not cause harm"),
23
+ pastel.red("━" * 80)
24
+ ].join("\n")
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+ require "tty-table"
5
+
6
+ module RubyCode
7
+ module Views
8
+ module Cli
9
+ # Builds configuration table display
10
+ class ConfigurationTable
11
+ def self.build(directory:, model:, debug_mode:)
12
+ pastel = Pastel.new
13
+
14
+ table = TTY::Table.new(
15
+ header: [pastel.bold("Setting"), pastel.bold("Value")],
16
+ rows: [
17
+ ["Directory", directory],
18
+ ["Model", model],
19
+ ["Debug Mode", debug_mode ? pastel.green("ON") : pastel.dim("OFF")]
20
+ ]
21
+ )
22
+
23
+ "\n#{table.render(:unicode, padding: [0, 1])}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds error display with backtrace
9
+ class ErrorDisplay
10
+ def self.build(error:)
11
+ pastel = Pastel.new
12
+ backtrace_preview = error.backtrace.first(3).join("\n")
13
+
14
+ "\n#{pastel.red("[ERROR]")} #{error.message}\n#{pastel.dim(backtrace_preview)}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds error message display
9
+ class ErrorMessage
10
+ def self.build(message:)
11
+ pastel = Pastel.new
12
+ "\n#{pastel.red("[ERROR]")} #{message}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds goodbye/exit message
9
+ class ExitMessage
10
+ def self.build
11
+ pastel = Pastel.new
12
+ "\n#{pastel.green(I18n.t("rubycode.cli.exit"))}\n"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds interrupt notification message
9
+ class InterruptMessage
10
+ def self.build
11
+ pastel = Pastel.new
12
+ "\n#{pastel.yellow("[INTERRUPTED]")} Type 'exit' to quit or continue chatting."
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds memory cleared confirmation message
9
+ class MemoryClearedMessage
10
+ def self.build
11
+ pastel = Pastel.new
12
+ "#{pastel.yellow("✓")} #{I18n.t("rubycode.cli.memory_cleared")}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds ready status message
9
+ class ReadyMessage
10
+ def self.build
11
+ pastel = Pastel.new
12
+ "\n#{pastel.green("✓")} #{pastel.bold("Ready!")} #{I18n.t("rubycode.cli.ready")}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+ require "tty-markdown"
5
+
6
+ module RubyCode
7
+ module Views
8
+ module Cli
9
+ # Builds response box with markdown rendering
10
+ class ResponseBox
11
+ def self.build(response:)
12
+ pastel = Pastel.new
13
+
14
+ header = "\n#{pastel.magenta("╔═══ Agent Response ═══")}"
15
+ footer = pastel.magenta("╚══════════════════════")
16
+
17
+ # Try to parse as markdown, fallback to plain
18
+ content = begin
19
+ TTY::Markdown.parse(response, width: 80)
20
+ rescue StandardError
21
+ response
22
+ end
23
+
24
+ "#{header}\n#{content}\n#{footer}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CLI view components
4
+ require_relative "cli/error_message"
5
+ require_relative "cli/configuration_table"
6
+ require_relative "cli/ready_message"
7
+ require_relative "cli/exit_message"
8
+ require_relative "cli/memory_cleared_message"
9
+ require_relative "cli/response_box"
10
+ require_relative "cli/interrupt_message"
11
+ require_relative "cli/error_display"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Formatter
8
+ # Builds debug mode tool information display
9
+ class DebugToolInfo
10
+ def self.build(tool_name:, arguments:)
11
+ pastel = Pastel.new
12
+ "\n#{pastel.yellow("[TOOL]")} #{tool_name}\n #{pastel.dim("Args:")} #{arguments.inspect}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Formatter
8
+ # Builds info message display
9
+ class InfoMessage
10
+ def self.build(message:)
11
+ pastel = Pastel.new
12
+ " #{pastel.dim("ℹ #{message}")}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+ require "tty-table"
5
+
6
+ module RubyCode
7
+ module Views
8
+ module Formatter
9
+ # Builds minimal tool info table display
10
+ class MinimalToolInfo
11
+ def self.build(label:, value:)
12
+ pastel = Pastel.new
13
+
14
+ table = TTY::Table.new(rows: [
15
+ [
16
+ pastel.cyan(label),
17
+ value
18
+ ]
19
+ ])
20
+
21
+ " #{table.render(:basic, padding: [0, 1])}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Formatter
8
+ # Builds tool result summary display
9
+ class ToolResult
10
+ def self.build(result:)
11
+ pastel = Pastel.new
12
+ first_line = result.lines.first&.strip || "(empty)"
13
+ suffix = result.lines.count > 1 ? "... (#{result.lines.count} lines)" : ""
14
+
15
+ " #{pastel.green("✓")} #{pastel.dim("Result:")} #{first_line}#{suffix}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Display formatter view components
4
+ require_relative "formatter/debug_tool_info"
5
+ require_relative "formatter/tool_result"
6
+ require_relative "formatter/info_message"
7
+ require_relative "formatter/minimal_tool_info"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-box"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module ResponseHandler
8
+ # Builds agent finished success box
9
+ class AgentFinished
10
+ def self.build(iteration:, total_tool_calls:)
11
+ success_box = TTY::Box.frame(
12
+ title: { top_left: " #{I18n.t("rubycode.response_handler.agent_finished.title")} " },
13
+ border: :light,
14
+ padding: 1,
15
+ style: {
16
+ fg: :green,
17
+ border: {
18
+ fg: :green
19
+ }
20
+ }
21
+ ) do
22
+ I18n.t("rubycode.response_handler.agent_finished.message",
23
+ iterations: iteration,
24
+ tool_calls: total_tool_calls)
25
+ end
26
+ "\n#{success_box}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-box"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module ResponseHandler
8
+ # Builds task completion success box
9
+ class CompleteMessage
10
+ def self.build(iteration:, total_tool_calls:)
11
+ success_box = TTY::Box.frame(
12
+ title: { top_left: " #{I18n.t("rubycode.response_handler.complete.title")} " },
13
+ border: :light,
14
+ padding: 1,
15
+ style: {
16
+ fg: :green,
17
+ border: {
18
+ fg: :green
19
+ }
20
+ }
21
+ ) do
22
+ I18n.t("rubycode.response_handler.complete.message",
23
+ iterations: iteration,
24
+ tool_calls: total_tool_calls)
25
+ end
26
+ "\n#{success_box}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-box"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module ResponseHandler
8
+ # Builds max iterations warning box
9
+ class MaxIterations
10
+ def self.build(max_iterations:)
11
+ error_box = TTY::Box.frame(
12
+ title: { top_left: " #{I18n.t("rubycode.response_handler.max_iterations.title")} " },
13
+ border: :thick,
14
+ padding: 1,
15
+ style: {
16
+ fg: :yellow,
17
+ border: {
18
+ fg: :yellow
19
+ }
20
+ }
21
+ ) do
22
+ I18n.t("rubycode.response_handler.max_iterations.message", max: max_iterations)
23
+ end
24
+ "\n#{error_box}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-box"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module ResponseHandler
8
+ # Builds max tool calls warning box
9
+ class MaxToolCalls
10
+ def self.build(max_tool_calls:)
11
+ error_box = TTY::Box.frame(
12
+ title: { top_left: " ⚠ WARNING " },
13
+ border: :thick,
14
+ padding: 1,
15
+ style: {
16
+ fg: :yellow,
17
+ border: {
18
+ fg: :yellow
19
+ }
20
+ }
21
+ ) do
22
+ "Reached maximum tool calls (#{max_tool_calls})\nStopping to prevent excessive operations."
23
+ end
24
+ "\n#{error_box}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module ResponseHandler
8
+ # Builds tool injection warning message
9
+ class ToolInjectionWarning
10
+ def self.build(iteration:)
11
+ pastel = Pastel.new
12
+ " #{pastel.yellow("[WARNING]")} No tool calls - injecting reminder (iteration #{iteration})"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Response handler view components
4
+ require_relative "response_handler/max_iterations"
5
+ require_relative "response_handler/max_tool_calls"
6
+ require_relative "response_handler/complete_message"
7
+ require_relative "response_handler/agent_finished"
8
+ require_relative "response_handler/tool_injection_warning"