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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +269 -467
  3. data/db/migrations/009_create_teams.sql +6 -6
  4. data/db/migrations/011_fix_mailbox_messages_columns.rb +35 -0
  5. data/db/migrations/012_expand_mailbox_message_types.rb +37 -0
  6. data/exe/rubyn-code +1 -1
  7. data/lib/rubyn_code/agent/RUBYN.md +17 -0
  8. data/lib/rubyn_code/agent/conversation.rb +68 -19
  9. data/lib/rubyn_code/agent/loop.rb +312 -54
  10. data/lib/rubyn_code/agent/loop_detector.rb +6 -6
  11. data/lib/rubyn_code/auth/RUBYN.md +19 -0
  12. data/lib/rubyn_code/auth/oauth.rb +40 -35
  13. data/lib/rubyn_code/auth/server.rb +16 -12
  14. data/lib/rubyn_code/auth/token_store.rb +22 -22
  15. data/lib/rubyn_code/autonomous/RUBYN.md +14 -0
  16. data/lib/rubyn_code/autonomous/daemon.rb +115 -79
  17. data/lib/rubyn_code/autonomous/idle_poller.rb +4 -8
  18. data/lib/rubyn_code/autonomous/task_claimer.rb +11 -11
  19. data/lib/rubyn_code/background/RUBYN.md +13 -0
  20. data/lib/rubyn_code/background/notifier.rb +0 -2
  21. data/lib/rubyn_code/background/worker.rb +60 -15
  22. data/lib/rubyn_code/cli/RUBYN.md +30 -0
  23. data/lib/rubyn_code/cli/app.rb +85 -9
  24. data/lib/rubyn_code/cli/commands/RUBYN.md +133 -0
  25. data/lib/rubyn_code/cli/commands/base.rb +53 -0
  26. data/lib/rubyn_code/cli/commands/budget.rb +24 -0
  27. data/lib/rubyn_code/cli/commands/clear.rb +16 -0
  28. data/lib/rubyn_code/cli/commands/compact.rb +21 -0
  29. data/lib/rubyn_code/cli/commands/context.rb +44 -0
  30. data/lib/rubyn_code/cli/commands/context_info.rb +56 -0
  31. data/lib/rubyn_code/cli/commands/cost.rb +23 -0
  32. data/lib/rubyn_code/cli/commands/diff.rb +30 -0
  33. data/lib/rubyn_code/cli/commands/doctor.rb +112 -0
  34. data/lib/rubyn_code/cli/commands/help.rb +41 -0
  35. data/lib/rubyn_code/cli/commands/model.rb +37 -0
  36. data/lib/rubyn_code/cli/commands/plan.rb +22 -0
  37. data/lib/rubyn_code/cli/commands/quit.rb +17 -0
  38. data/lib/rubyn_code/cli/commands/registry.rb +64 -0
  39. data/lib/rubyn_code/cli/commands/resume.rb +51 -0
  40. data/lib/rubyn_code/cli/commands/review.rb +26 -0
  41. data/lib/rubyn_code/cli/commands/skill.rb +32 -0
  42. data/lib/rubyn_code/cli/commands/spawn.rb +24 -0
  43. data/lib/rubyn_code/cli/commands/tasks.rb +32 -0
  44. data/lib/rubyn_code/cli/commands/tokens.rb +76 -0
  45. data/lib/rubyn_code/cli/commands/undo.rb +17 -0
  46. data/lib/rubyn_code/cli/commands/version.rb +16 -0
  47. data/lib/rubyn_code/cli/daemon_runner.rb +129 -0
  48. data/lib/rubyn_code/cli/input_handler.rb +20 -23
  49. data/lib/rubyn_code/cli/renderer.rb +25 -27
  50. data/lib/rubyn_code/cli/repl.rb +161 -194
  51. data/lib/rubyn_code/cli/setup.rb +117 -0
  52. data/lib/rubyn_code/cli/spinner.rb +40 -40
  53. data/lib/rubyn_code/cli/stream_formatter.rb +29 -28
  54. data/lib/rubyn_code/cli/version_check.rb +94 -0
  55. data/lib/rubyn_code/config/RUBYN.md +14 -0
  56. data/lib/rubyn_code/config/defaults.rb +28 -19
  57. data/lib/rubyn_code/config/project_config.rb +7 -9
  58. data/lib/rubyn_code/config/settings.rb +3 -3
  59. data/lib/rubyn_code/context/RUBYN.md +20 -0
  60. data/lib/rubyn_code/context/auto_compact.rb +7 -7
  61. data/lib/rubyn_code/context/compactor.rb +2 -2
  62. data/lib/rubyn_code/context/context_collapse.rb +45 -0
  63. data/lib/rubyn_code/context/manager.rb +20 -3
  64. data/lib/rubyn_code/context/manual_compact.rb +7 -7
  65. data/lib/rubyn_code/context/micro_compact.rb +12 -12
  66. data/lib/rubyn_code/db/RUBYN.md +40 -0
  67. data/lib/rubyn_code/db/connection.rb +13 -13
  68. data/lib/rubyn_code/db/migrator.rb +67 -27
  69. data/lib/rubyn_code/db/schema.rb +6 -6
  70. data/lib/rubyn_code/debug.rb +74 -0
  71. data/lib/rubyn_code/hooks/RUBYN.md +17 -0
  72. data/lib/rubyn_code/hooks/built_in.rb +9 -9
  73. data/lib/rubyn_code/hooks/registry.rb +5 -5
  74. data/lib/rubyn_code/hooks/runner.rb +1 -1
  75. data/lib/rubyn_code/hooks/user_hooks.rb +16 -16
  76. data/lib/rubyn_code/learning/RUBYN.md +16 -0
  77. data/lib/rubyn_code/learning/extractor.rb +22 -22
  78. data/lib/rubyn_code/learning/injector.rb +17 -18
  79. data/lib/rubyn_code/learning/instinct.rb +18 -14
  80. data/lib/rubyn_code/llm/RUBYN.md +15 -0
  81. data/lib/rubyn_code/llm/client.rb +121 -55
  82. data/lib/rubyn_code/llm/message_builder.rb +19 -15
  83. data/lib/rubyn_code/llm/streaming.rb +80 -50
  84. data/lib/rubyn_code/mcp/RUBYN.md +21 -0
  85. data/lib/rubyn_code/mcp/client.rb +25 -24
  86. data/lib/rubyn_code/mcp/config.rb +7 -7
  87. data/lib/rubyn_code/mcp/sse_transport.rb +27 -26
  88. data/lib/rubyn_code/mcp/stdio_transport.rb +22 -19
  89. data/lib/rubyn_code/mcp/tool_bridge.rb +32 -32
  90. data/lib/rubyn_code/memory/RUBYN.md +17 -0
  91. data/lib/rubyn_code/memory/models.rb +3 -3
  92. data/lib/rubyn_code/memory/search.rb +17 -17
  93. data/lib/rubyn_code/memory/session_persistence.rb +49 -34
  94. data/lib/rubyn_code/memory/store.rb +17 -17
  95. data/lib/rubyn_code/observability/RUBYN.md +19 -0
  96. data/lib/rubyn_code/observability/budget_enforcer.rb +16 -15
  97. data/lib/rubyn_code/observability/cost_calculator.rb +3 -3
  98. data/lib/rubyn_code/observability/token_counter.rb +1 -1
  99. data/lib/rubyn_code/observability/usage_reporter.rb +35 -35
  100. data/lib/rubyn_code/output/RUBYN.md +11 -0
  101. data/lib/rubyn_code/output/diff_renderer.rb +6 -6
  102. data/lib/rubyn_code/output/formatter.rb +4 -4
  103. data/lib/rubyn_code/permissions/RUBYN.md +17 -0
  104. data/lib/rubyn_code/permissions/prompter.rb +8 -8
  105. data/lib/rubyn_code/protocols/RUBYN.md +14 -0
  106. data/lib/rubyn_code/protocols/interrupt_handler.rb +1 -1
  107. data/lib/rubyn_code/protocols/plan_approval.rb +9 -9
  108. data/lib/rubyn_code/protocols/shutdown_handshake.rb +9 -11
  109. data/lib/rubyn_code/skills/RUBYN.md +19 -0
  110. data/lib/rubyn_code/skills/catalog.rb +7 -7
  111. data/lib/rubyn_code/skills/document.rb +15 -15
  112. data/lib/rubyn_code/skills/loader.rb +6 -8
  113. data/lib/rubyn_code/sub_agents/RUBYN.md +12 -0
  114. data/lib/rubyn_code/sub_agents/runner.rb +15 -15
  115. data/lib/rubyn_code/sub_agents/summarizer.rb +1 -1
  116. data/lib/rubyn_code/tasks/RUBYN.md +13 -0
  117. data/lib/rubyn_code/tasks/dag.rb +12 -16
  118. data/lib/rubyn_code/tasks/manager.rb +24 -24
  119. data/lib/rubyn_code/tasks/models.rb +4 -4
  120. data/lib/rubyn_code/teams/RUBYN.md +14 -0
  121. data/lib/rubyn_code/teams/mailbox.rb +38 -18
  122. data/lib/rubyn_code/teams/manager.rb +19 -19
  123. data/lib/rubyn_code/teams/teammate.rb +3 -4
  124. data/lib/rubyn_code/tools/RUBYN.md +38 -0
  125. data/lib/rubyn_code/tools/background_run.rb +9 -11
  126. data/lib/rubyn_code/tools/base.rb +54 -3
  127. data/lib/rubyn_code/tools/bash.rb +16 -34
  128. data/lib/rubyn_code/tools/bundle_add.rb +10 -12
  129. data/lib/rubyn_code/tools/bundle_install.rb +9 -11
  130. data/lib/rubyn_code/tools/compact.rb +10 -9
  131. data/lib/rubyn_code/tools/db_migrate.rb +17 -15
  132. data/lib/rubyn_code/tools/edit_file.rb +12 -12
  133. data/lib/rubyn_code/tools/executor.rb +9 -4
  134. data/lib/rubyn_code/tools/git_commit.rb +29 -34
  135. data/lib/rubyn_code/tools/git_diff.rb +17 -18
  136. data/lib/rubyn_code/tools/git_log.rb +17 -19
  137. data/lib/rubyn_code/tools/git_status.rb +18 -20
  138. data/lib/rubyn_code/tools/glob.rb +7 -9
  139. data/lib/rubyn_code/tools/grep.rb +11 -9
  140. data/lib/rubyn_code/tools/load_skill.rb +7 -7
  141. data/lib/rubyn_code/tools/memory_search.rb +13 -12
  142. data/lib/rubyn_code/tools/memory_write.rb +14 -12
  143. data/lib/rubyn_code/tools/rails_generate.rb +16 -16
  144. data/lib/rubyn_code/tools/read_file.rb +8 -7
  145. data/lib/rubyn_code/tools/read_inbox.rb +5 -5
  146. data/lib/rubyn_code/tools/registry.rb +2 -2
  147. data/lib/rubyn_code/tools/review_pr.rb +55 -55
  148. data/lib/rubyn_code/tools/run_specs.rb +20 -19
  149. data/lib/rubyn_code/tools/schema.rb +9 -11
  150. data/lib/rubyn_code/tools/send_message.rb +10 -10
  151. data/lib/rubyn_code/tools/spawn_agent.rb +51 -23
  152. data/lib/rubyn_code/tools/spawn_teammate.rb +21 -21
  153. data/lib/rubyn_code/tools/task.rb +28 -28
  154. data/lib/rubyn_code/tools/web_fetch.rb +46 -31
  155. data/lib/rubyn_code/tools/web_search.rb +64 -66
  156. data/lib/rubyn_code/tools/write_file.rb +7 -6
  157. data/lib/rubyn_code/version.rb +1 -1
  158. data/lib/rubyn_code.rb +136 -105
  159. metadata +94 -21
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require_relative "base"
5
- require_relative "registry"
3
+ require 'open3'
4
+ require_relative 'base'
5
+ require_relative 'registry'
6
6
 
7
7
  module RubynCode
8
8
  module Tools
9
9
  class BundleAdd < Base
10
- TOOL_NAME = "bundle_add"
11
- DESCRIPTION = "Adds a gem to the Gemfile and installs it via `bundle add`."
10
+ TOOL_NAME = 'bundle_add'
11
+ DESCRIPTION = 'Adds a gem to the Gemfile and installs it via `bundle add`.'
12
12
  PARAMETERS = {
13
- gem_name: { type: :string, required: true, description: "Name of the gem to add" },
13
+ gem_name: { type: :string, required: true, description: 'Name of the gem to add' },
14
14
  version: { type: :string, required: false, description: "Version constraint (e.g. '~> 1.0')" },
15
15
  group: { type: :string, required: false, description: "Gemfile group (e.g. 'development', 'test')" }
16
16
  }.freeze
@@ -18,14 +18,12 @@ module RubynCode
18
18
  REQUIRES_CONFIRMATION = false
19
19
 
20
20
  def execute(gem_name:, version: nil, group: nil)
21
- gemfile_path = File.join(project_root, "Gemfile")
21
+ gemfile_path = File.join(project_root, 'Gemfile')
22
22
 
23
- unless File.exist?(gemfile_path)
24
- raise Error, "No Gemfile found in project root. Cannot run bundle add."
25
- end
23
+ raise Error, 'No Gemfile found in project root. Cannot run bundle add.' unless File.exist?(gemfile_path)
26
24
 
27
25
  command = build_command(gem_name, version, group)
28
- stdout, stderr, status = Open3.capture3(command, chdir: project_root)
26
+ stdout, stderr, status = safe_capture3(command, chdir: project_root)
29
27
 
30
28
  build_output(stdout, stderr, status)
31
29
  end
@@ -44,7 +42,7 @@ module RubynCode
44
42
  parts << stdout unless stdout.empty?
45
43
  parts << "STDERR:\n#{stderr}" unless stderr.empty?
46
44
  parts << "Exit code: #{status.exitstatus}" unless status.success?
47
- parts.empty? ? "(no output)" : parts.join("\n")
45
+ parts.empty? ? '(no output)' : parts.join("\n")
48
46
  end
49
47
  end
50
48
 
@@ -1,26 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require_relative "base"
5
- require_relative "registry"
3
+ require 'open3'
4
+ require_relative 'base'
5
+ require_relative 'registry'
6
6
 
7
7
  module RubynCode
8
8
  module Tools
9
9
  class BundleInstall < Base
10
- TOOL_NAME = "bundle_install"
11
- DESCRIPTION = "Runs `bundle install` to install gem dependencies."
10
+ TOOL_NAME = 'bundle_install'
11
+ DESCRIPTION = 'Runs `bundle install` to install gem dependencies.'
12
12
  PARAMETERS = {}.freeze
13
13
  RISK_LEVEL = :execute
14
14
  REQUIRES_CONFIRMATION = false
15
15
 
16
16
  def execute(**_params)
17
- gemfile_path = File.join(project_root, "Gemfile")
17
+ gemfile_path = File.join(project_root, 'Gemfile')
18
18
 
19
- unless File.exist?(gemfile_path)
20
- raise Error, "No Gemfile found in project root. Cannot run bundle install."
21
- end
19
+ raise Error, 'No Gemfile found in project root. Cannot run bundle install.' unless File.exist?(gemfile_path)
22
20
 
23
- stdout, stderr, status = Open3.capture3("bundle install", chdir: project_root)
21
+ stdout, stderr, status = safe_capture3('bundle install', chdir: project_root)
24
22
 
25
23
  build_output(stdout, stderr, status)
26
24
  end
@@ -32,7 +30,7 @@ module RubynCode
32
30
  parts << stdout unless stdout.empty?
33
31
  parts << "STDERR:\n#{stderr}" unless stderr.empty?
34
32
  parts << "Exit code: #{status.exitstatus}" unless status.success?
35
- parts.empty? ? "(no output)" : parts.join("\n")
33
+ parts.empty? ? '(no output)' : parts.join("\n")
36
34
  end
37
35
  end
38
36
 
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
- require_relative "registry"
3
+ require_relative 'base'
4
+ require_relative 'registry'
5
5
 
6
6
  module RubynCode
7
7
  module Tools
8
8
  class Compact < Base
9
- TOOL_NAME = "compact"
10
- DESCRIPTION = "Triggers manual context compaction to reduce conversation size while preserving key information."
9
+ TOOL_NAME = 'compact'
10
+ DESCRIPTION = 'Triggers manual context compaction to reduce conversation size while preserving key information.'
11
11
  PARAMETERS = {
12
- focus: { type: :string, required: false, description: "What to focus the summary on (e.g. 'the auth refactor', 'test failures')" }
12
+ focus: { type: :string, required: false,
13
+ description: "What to focus the summary on (e.g. 'the auth refactor', 'test failures')" }
13
14
  }.freeze
14
15
  RISK_LEVEL = :read
15
16
  REQUIRES_CONFIRMATION = false
@@ -23,22 +24,22 @@ module RubynCode
23
24
  manager = @context_manager
24
25
 
25
26
  unless manager
26
- return "Context compaction is not available in this session. " \
27
- "No context manager was provided."
27
+ return 'Context compaction is not available in this session. ' \
28
+ 'No context manager was provided.'
28
29
  end
29
30
 
30
31
  if manager.respond_to?(:compact)
31
32
  result = manager.compact(focus: focus)
32
33
  format_result(result, focus)
33
34
  else
34
- "Context manager does not support compaction."
35
+ 'Context manager does not support compaction.'
35
36
  end
36
37
  end
37
38
 
38
39
  private
39
40
 
40
41
  def format_result(result, focus)
41
- parts = ["Context compacted successfully."]
42
+ parts = ['Context compacted successfully.']
42
43
 
43
44
  if result.is_a?(Hash)
44
45
  parts << "Messages before: #{result[:before]}" if result[:before]
@@ -1,24 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require_relative "base"
5
- require_relative "registry"
3
+ require 'open3'
4
+ require_relative 'base'
5
+ require_relative 'registry'
6
6
 
7
7
  module RubynCode
8
8
  module Tools
9
9
  class DbMigrate < Base
10
- TOOL_NAME = "db_migrate"
11
- DESCRIPTION = "Runs Rails database migrations (up) or rollback (down)."
10
+ TOOL_NAME = 'db_migrate'
11
+ DESCRIPTION = 'Runs Rails database migrations (up) or rollback (down).'
12
12
  PARAMETERS = {
13
- direction: { type: :string, required: false, default: "up", enum: %w[up down], description: "Migration direction: 'up' to migrate, 'down' to rollback (default: 'up')" },
14
- steps: { type: :integer, required: false, description: "Number of steps to rollback (only used with direction 'down')" }
13
+ direction: { type: :string, required: false, default: 'up', enum: %w[up down],
14
+ description: "Migration direction: 'up' to migrate, 'down' to rollback (default: 'up')" },
15
+ steps: { type: :integer, required: false,
16
+ description: "Number of steps to rollback (only used with direction 'down')" }
15
17
  }.freeze
16
18
  RISK_LEVEL = :execute
17
19
  REQUIRES_CONFIRMATION = false
18
20
 
19
- def execute(direction: "up", steps: nil)
21
+ def execute(direction: 'up', steps: nil)
20
22
  command = build_command(direction, steps)
21
- stdout, stderr, status = Open3.capture3(command, chdir: project_root)
23
+ stdout, stderr, status = safe_capture3(command, chdir: project_root)
22
24
 
23
25
  build_output(stdout, stderr, status)
24
26
  end
@@ -27,11 +29,11 @@ module RubynCode
27
29
 
28
30
  def build_command(direction, steps)
29
31
  case direction
30
- when "up"
31
- "bundle exec rails db:migrate"
32
- when "down"
33
- cmd = "bundle exec rails db:rollback"
34
- cmd += " STEP=#{steps.to_i}" if steps && steps.to_i > 0
32
+ when 'up'
33
+ 'bundle exec rails db:migrate'
34
+ when 'down'
35
+ cmd = 'bundle exec rails db:rollback'
36
+ cmd += " STEP=#{steps.to_i}" if steps&.to_i&.positive?
35
37
  cmd
36
38
  else
37
39
  raise Error, "Invalid direction: #{direction}. Must be 'up' or 'down'."
@@ -43,7 +45,7 @@ module RubynCode
43
45
  parts << stdout unless stdout.empty?
44
46
  parts << "STDERR:\n#{stderr}" unless stderr.empty?
45
47
  parts << "Exit code: #{status.exitstatus}" unless status.success?
46
- parts.empty? ? "(no output)" : parts.join("\n")
48
+ parts.empty? ? '(no output)' : parts.join("\n")
47
49
  end
48
50
  end
49
51
 
@@ -1,18 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
- require_relative "registry"
3
+ require_relative 'base'
4
+ require_relative 'registry'
5
5
 
6
6
  module RubynCode
7
7
  module Tools
8
8
  class EditFile < Base
9
- TOOL_NAME = "edit_file"
10
- DESCRIPTION = "Performs exact string replacement in a file. Fails if old_text is not found or is ambiguous."
9
+ TOOL_NAME = 'edit_file'
10
+ DESCRIPTION = 'Performs exact string replacement in a file. Fails if old_text is not found or is ambiguous.'
11
11
  PARAMETERS = {
12
- path: { type: :string, required: true, description: "Path to the file to edit" },
13
- old_text: { type: :string, required: true, description: "The exact text to find and replace" },
14
- new_text: { type: :string, required: true, description: "The replacement text" },
15
- replace_all: { type: :boolean, required: false, default: false, description: "Replace all occurrences (default: false)" }
12
+ path: { type: :string, required: true, description: 'Path to the file to edit' },
13
+ old_text: { type: :string, required: true, description: 'The exact text to find and replace' },
14
+ new_text: { type: :string, required: true, description: 'The replacement text' },
15
+ replace_all: { type: :boolean, required: false, default: false,
16
+ description: 'Replace all occurrences (default: false)' }
16
17
  }.freeze
17
18
  RISK_LEVEL = :write
18
19
  REQUIRES_CONFIRMATION = false
@@ -23,12 +24,11 @@ module RubynCode
23
24
 
24
25
  occurrences = content.scan(old_text).length
25
26
 
26
- if occurrences.zero?
27
- raise Error, "old_text not found in #{path}. No changes made."
28
- end
27
+ raise Error, "old_text not found in #{path}. No changes made." if occurrences.zero?
29
28
 
30
29
  if !replace_all && occurrences > 1
31
- raise Error, "old_text found #{occurrences} times in #{path}. Use replace_all: true to replace all, or provide a more specific old_text."
30
+ raise Error,
31
+ "old_text found #{occurrences} times in #{path}. Use replace_all: true to replace all, or provide a more specific old_text."
32
32
  end
33
33
 
34
34
  new_content = if replace_all
@@ -20,7 +20,12 @@ module RubynCode
20
20
  inject_dependencies(tool, tool_name)
21
21
 
22
22
  symbolized = params.transform_keys(&:to_sym)
23
- result = tool.execute(**symbolized)
23
+ # Filter to only params the tool's execute method accepts — LLM may send extra keys
24
+ allowed = tool.method(:execute).parameters
25
+ .slice(:key, :keyreq)
26
+ .map(&:last)
27
+ filtered = allowed.empty? ? symbolized : symbolized.slice(*allowed)
28
+ result = tool.execute(**filtered)
24
29
  tool.truncate(result.to_s)
25
30
  rescue ToolNotFoundError => e
26
31
  error_result("Tool error: #{e.message}")
@@ -42,14 +47,14 @@ module RubynCode
42
47
 
43
48
  def inject_dependencies(tool, tool_name)
44
49
  case tool_name
45
- when "spawn_agent"
50
+ when 'spawn_agent'
46
51
  tool.llm_client = @llm_client if tool.respond_to?(:llm_client=)
47
52
  tool.on_status = @on_agent_status if tool.respond_to?(:on_status=)
48
- when "spawn_teammate"
53
+ when 'spawn_teammate'
49
54
  tool.llm_client = @llm_client if tool.respond_to?(:llm_client=)
50
55
  tool.on_status = @on_agent_status if tool.respond_to?(:on_status=)
51
56
  tool.db = @db if tool.respond_to?(:db=)
52
- when "background_run"
57
+ when 'background_run'
53
58
  tool.background_worker = @background_worker if tool.respond_to?(:background_worker=)
54
59
  end
55
60
  end
@@ -1,70 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require_relative "base"
5
- require_relative "registry"
3
+ require 'open3'
4
+ require_relative 'base'
5
+ require_relative 'registry'
6
6
 
7
7
  module RubynCode
8
8
  module Tools
9
9
  class GitCommit < Base
10
- TOOL_NAME = "git_commit"
10
+ TOOL_NAME = 'git_commit'
11
11
  DESCRIPTION = "Stage files and create a git commit. Specify files to stage or use 'all' to stage everything."
12
12
  PARAMETERS = {
13
- message: { type: :string, required: true, description: "The commit message" },
14
- files: { type: :string, required: false, default: "all", description: "Space-separated file paths to stage, or 'all' to stage everything (git add -A)" }
13
+ message: { type: :string, required: true, description: 'The commit message' },
14
+ files: { type: :string, required: false, default: 'all',
15
+ description: "Space-separated file paths to stage, or 'all' to stage everything (git add -A)" }
15
16
  }.freeze
16
17
  RISK_LEVEL = :write
17
18
  REQUIRES_CONFIRMATION = true
18
19
 
19
- def execute(message:, files: "all")
20
+ def execute(message:, files: 'all')
20
21
  validate_git_repo!
21
22
  validate_message!(message)
22
23
 
23
24
  stage_files(files)
24
- commit_output = create_commit(message)
25
-
26
- commit_output
25
+ create_commit(message)
27
26
  end
28
27
 
29
28
  private
30
29
 
31
30
  def validate_git_repo!
32
- _, _, status = Open3.capture3("git", "rev-parse", "--is-inside-work-tree", chdir: project_root)
33
- unless status.success?
34
- raise Error, "Not a git repository: #{project_root}"
35
- end
31
+ _, _, status = safe_capture3('git', 'rev-parse', '--is-inside-work-tree', chdir: project_root)
32
+ return if status.success?
33
+
34
+ raise Error, "Not a git repository: #{project_root}"
36
35
  end
37
36
 
38
37
  def validate_message!(message)
39
- if message.nil? || message.strip.empty?
40
- raise Error, "Commit message cannot be empty"
41
- end
38
+ return unless message.nil? || message.strip.empty?
39
+
40
+ raise Error, 'Commit message cannot be empty'
42
41
  end
43
42
 
44
43
  def stage_files(files)
45
- if files.strip.downcase == "all"
46
- stdout, stderr, status = Open3.capture3("git", "add", "-A", chdir: project_root)
44
+ if files.strip.downcase == 'all'
45
+ _, stderr, status = safe_capture3('git', 'add', '-A', chdir: project_root)
47
46
  else
48
47
  file_list = files.split(/\s+/).reject(&:empty?)
49
- if file_list.empty?
50
- raise Error, "No files specified to stage"
51
- end
48
+ raise Error, 'No files specified to stage' if file_list.empty?
52
49
 
53
- stdout, stderr, status = Open3.capture3("git", "add", "--", *file_list, chdir: project_root)
50
+ _, stderr, status = safe_capture3('git', 'add', '--', *file_list, chdir: project_root)
54
51
  end
55
52
 
56
- unless status.success?
57
- raise Error, "Failed to stage files: #{stderr.strip}"
58
- end
53
+ return if status.success?
54
+
55
+ raise Error, "Failed to stage files: #{stderr.strip}"
59
56
  end
60
57
 
61
58
  def create_commit(message)
62
- stdout, stderr, status = Open3.capture3("git", "commit", "-m", message, chdir: project_root)
59
+ stdout, stderr, status = safe_capture3('git', 'commit', '-m', message, chdir: project_root)
63
60
 
64
61
  unless status.success?
65
- if stderr.include?("nothing to commit")
66
- return "Nothing to commit — working tree is clean."
67
- end
62
+ return 'Nothing to commit — working tree is clean.' if stderr.include?('nothing to commit')
68
63
 
69
64
  raise Error, "Commit failed: #{stderr.strip}"
70
65
  end
@@ -75,20 +70,20 @@ module RubynCode
75
70
 
76
71
  lines = ["Committed on branch: #{branch}"]
77
72
  lines << "Commit: #{commit_hash}" if commit_hash
78
- lines << ""
73
+ lines << ''
79
74
  lines << stdout.strip
80
75
 
81
76
  lines.join("\n")
82
77
  end
83
78
 
84
79
  def extract_commit_hash
85
- stdout, _, status = Open3.capture3("git", "rev-parse", "--short", "HEAD", chdir: project_root)
80
+ stdout, _, status = safe_capture3('git', 'rev-parse', '--short', 'HEAD', chdir: project_root)
86
81
  status.success? ? stdout.strip : nil
87
82
  end
88
83
 
89
84
  def current_branch
90
- stdout, _, status = Open3.capture3("git", "branch", "--show-current", chdir: project_root)
91
- status.success? && !stdout.strip.empty? ? stdout.strip : "HEAD (detached)"
85
+ stdout, _, status = safe_capture3('git', 'branch', '--show-current', chdir: project_root)
86
+ status.success? && !stdout.strip.empty? ? stdout.strip : 'HEAD (detached)'
92
87
  end
93
88
  end
94
89
 
@@ -1,31 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require_relative "base"
5
- require_relative "registry"
3
+ require 'open3'
4
+ require_relative 'base'
5
+ require_relative 'registry'
6
6
 
7
7
  module RubynCode
8
8
  module Tools
9
9
  class GitDiff < Base
10
- TOOL_NAME = "git_diff"
11
- DESCRIPTION = "Show git diff for staged, unstaged, or between branches/commits."
10
+ TOOL_NAME = 'git_diff'
11
+ DESCRIPTION = 'Show git diff for staged, unstaged, or between branches/commits.'
12
12
  PARAMETERS = {
13
- target: { type: :string, required: false, default: "unstaged", description: 'What to diff: "staged", "unstaged", or a branch/commit ref (default: "unstaged")' }
13
+ target: { type: :string, required: false, default: 'unstaged',
14
+ description: 'What to diff: "staged", "unstaged", or a branch/commit ref (default: "unstaged")' }
14
15
  }.freeze
15
16
  RISK_LEVEL = :read
16
17
  REQUIRES_CONFIRMATION = false
17
18
 
18
19
  MAX_DIFF_LENGTH = 80_000
19
20
 
20
- def execute(target: "unstaged")
21
+ def execute(target: 'unstaged')
21
22
  validate_git_repo!
22
23
 
23
24
  cmd = build_diff_command(target.to_s.strip)
24
- stdout, stderr, status = Open3.capture3(*cmd, chdir: project_root)
25
+ stdout, stderr, status = safe_capture3(*cmd, chdir: project_root)
25
26
 
26
- unless status.success?
27
- raise Error, "git diff failed: #{stderr.strip}"
28
- end
27
+ raise Error, "git diff failed: #{stderr.strip}" unless status.success?
29
28
 
30
29
  if stdout.strip.empty?
31
30
  "No differences found for target: #{target}"
@@ -38,20 +37,20 @@ module RubynCode
38
37
  private
39
38
 
40
39
  def validate_git_repo!
41
- _, _, status = Open3.capture3("git", "rev-parse", "--is-inside-work-tree", chdir: project_root)
42
- unless status.success?
43
- raise Error, "Not a git repository: #{project_root}"
44
- end
40
+ _, _, status = safe_capture3('git', 'rev-parse', '--is-inside-work-tree', chdir: project_root)
41
+ return if status.success?
42
+
43
+ raise Error, "Not a git repository: #{project_root}"
45
44
  end
46
45
 
47
46
  def build_diff_command(target)
48
47
  case target.downcase
49
- when "staged", "cached"
48
+ when 'staged', 'cached'
50
49
  %w[git diff --cached]
51
- when "unstaged", ""
50
+ when 'unstaged', ''
52
51
  %w[git diff]
53
52
  else
54
- ["git", "diff", target]
53
+ ['git', 'diff', target]
55
54
  end
56
55
  end
57
56
  end
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require_relative "base"
5
- require_relative "registry"
3
+ require 'open3'
4
+ require_relative 'base'
5
+ require_relative 'registry'
6
6
 
7
7
  module RubynCode
8
8
  module Tools
9
9
  class GitLog < Base
10
- TOOL_NAME = "git_log"
11
- DESCRIPTION = "Show recent git commit history."
10
+ TOOL_NAME = 'git_log'
11
+ DESCRIPTION = 'Show recent git commit history.'
12
12
  PARAMETERS = {
13
- count: { type: :integer, required: false, default: 20, description: "Number of commits to show (default: 20)" },
14
- branch: { type: :string, required: false, description: "Branch name to show log for (default: current branch)" }
13
+ count: { type: :integer, required: false, default: 20, description: 'Number of commits to show (default: 20)' },
14
+ branch: { type: :string, required: false, description: 'Branch name to show log for (default: current branch)' }
15
15
  }.freeze
16
16
  RISK_LEVEL = :read
17
17
  REQUIRES_CONFIRMATION = false
@@ -21,17 +21,15 @@ module RubynCode
21
21
 
22
22
  count = [[count.to_i, 1].max, 200].min
23
23
 
24
- cmd = ["git", "log", "--oneline", "-#{count}"]
24
+ cmd = ['git', 'log', '--oneline', "-#{count}"]
25
25
  cmd << branch unless branch.nil? || branch.strip.empty?
26
26
 
27
- stdout, stderr, status = Open3.capture3(*cmd, chdir: project_root)
27
+ stdout, stderr, status = safe_capture3(*cmd, chdir: project_root)
28
28
 
29
- unless status.success?
30
- raise Error, "git log failed: #{stderr.strip}"
31
- end
29
+ raise Error, "git log failed: #{stderr.strip}" unless status.success?
32
30
 
33
31
  if stdout.strip.empty?
34
- "No commits found."
32
+ 'No commits found.'
35
33
  else
36
34
  current = current_branch
37
35
  header = "Commit history#{branch ? " (#{branch})" : " (#{current})"}:\n\n"
@@ -42,15 +40,15 @@ module RubynCode
42
40
  private
43
41
 
44
42
  def validate_git_repo!
45
- _, _, status = Open3.capture3("git", "rev-parse", "--is-inside-work-tree", chdir: project_root)
46
- unless status.success?
47
- raise Error, "Not a git repository: #{project_root}"
48
- end
43
+ _, _, status = safe_capture3('git', 'rev-parse', '--is-inside-work-tree', chdir: project_root)
44
+ return if status.success?
45
+
46
+ raise Error, "Not a git repository: #{project_root}"
49
47
  end
50
48
 
51
49
  def current_branch
52
- stdout, _, status = Open3.capture3("git", "branch", "--show-current", chdir: project_root)
53
- status.success? && !stdout.strip.empty? ? stdout.strip : "HEAD (detached)"
50
+ stdout, _, status = safe_capture3('git', 'branch', '--show-current', chdir: project_root)
51
+ status.success? && !stdout.strip.empty? ? stdout.strip : 'HEAD (detached)'
54
52
  end
55
53
  end
56
54
 
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require_relative "base"
5
- require_relative "registry"
3
+ require 'open3'
4
+ require_relative 'base'
5
+ require_relative 'registry'
6
6
 
7
7
  module RubynCode
8
8
  module Tools
9
9
  class GitStatus < Base
10
- TOOL_NAME = "git_status"
11
- DESCRIPTION = "Show the current git status — modified, staged, and untracked files."
10
+ TOOL_NAME = 'git_status'
11
+ DESCRIPTION = 'Show the current git status — modified, staged, and untracked files.'
12
12
  PARAMETERS = {}.freeze
13
13
  RISK_LEVEL = :read
14
14
  REQUIRES_CONFIRMATION = false
@@ -21,11 +21,11 @@ module RubynCode
21
21
 
22
22
  lines = ["Branch: #{branch}\n"]
23
23
 
24
- if status_output.strip.empty?
25
- lines << "Working tree is clean — nothing to commit."
26
- else
27
- lines << status_output
28
- end
24
+ lines << if status_output.strip.empty?
25
+ 'Working tree is clean — nothing to commit.'
26
+ else
27
+ status_output
28
+ end
29
29
 
30
30
  lines.join("\n")
31
31
  end
@@ -33,22 +33,20 @@ module RubynCode
33
33
  private
34
34
 
35
35
  def validate_git_repo!
36
- _, _, status = Open3.capture3("git", "rev-parse", "--is-inside-work-tree", chdir: project_root)
37
- unless status.success?
38
- raise Error, "Not a git repository: #{project_root}"
39
- end
36
+ _, _, status = safe_capture3('git', 'rev-parse', '--is-inside-work-tree', chdir: project_root)
37
+ return if status.success?
38
+
39
+ raise Error, "Not a git repository: #{project_root}"
40
40
  end
41
41
 
42
42
  def current_branch
43
- stdout, _, status = Open3.capture3("git", "branch", "--show-current", chdir: project_root)
44
- status.success? && !stdout.strip.empty? ? stdout.strip : "HEAD (detached)"
43
+ stdout, _, status = safe_capture3('git', 'branch', '--show-current', chdir: project_root)
44
+ status.success? && !stdout.strip.empty? ? stdout.strip : 'HEAD (detached)'
45
45
  end
46
46
 
47
47
  def git_status
48
- stdout, stderr, status = Open3.capture3("git", "status", "--short", chdir: project_root)
49
- unless status.success?
50
- raise Error, "git status failed: #{stderr.strip}"
51
- end
48
+ stdout, stderr, status = safe_capture3('git', 'status', '--short', chdir: project_root)
49
+ raise Error, "git status failed: #{stderr.strip}" unless status.success?
52
50
 
53
51
  stdout
54
52
  end