rubyn-code 0.7.0 → 0.9.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -17
  3. data/lib/rubyn_code/agent/conversation.rb +11 -1
  4. data/lib/rubyn_code/agent/dynamic_tool_schema.rb +1 -1
  5. data/lib/rubyn_code/agent/llm_caller.rb +5 -1
  6. data/lib/rubyn_code/agent/loop.rb +57 -3
  7. data/lib/rubyn_code/agent/response_parser.rb +8 -0
  8. data/lib/rubyn_code/agent/system_prompt_builder.rb +3 -0
  9. data/lib/rubyn_code/agent/tool_processor.rb +10 -0
  10. data/lib/rubyn_code/autonomous/daemon.rb +1 -1
  11. data/lib/rubyn_code/cli/commands/context.rb +27 -0
  12. data/lib/rubyn_code/cli/commands/custom_command.rb +44 -2
  13. data/lib/rubyn_code/cli/commands/custom_loader.rb +36 -5
  14. data/lib/rubyn_code/cli/commands/effort.rb +47 -0
  15. data/lib/rubyn_code/cli/commands/export.rb +174 -0
  16. data/lib/rubyn_code/cli/commands/mcp.rb +32 -8
  17. data/lib/rubyn_code/cli/commands/resume.rb +97 -26
  18. data/lib/rubyn_code/cli/commands/think.rb +47 -0
  19. data/lib/rubyn_code/cli/first_run.rb +1 -1
  20. data/lib/rubyn_code/cli/mention_expander.rb +19 -0
  21. data/lib/rubyn_code/cli/repl.rb +31 -1
  22. data/lib/rubyn_code/cli/repl_commands.rb +1 -1
  23. data/lib/rubyn_code/cli/repl_setup.rb +8 -6
  24. data/lib/rubyn_code/config/defaults.rb +2 -1
  25. data/lib/rubyn_code/config/schema.json +5 -0
  26. data/lib/rubyn_code/config/settings.rb +4 -2
  27. data/lib/rubyn_code/context/auto_compact.rb +1 -1
  28. data/lib/rubyn_code/context/manual_compact.rb +1 -1
  29. data/lib/rubyn_code/index/codebase_index.rb +64 -3
  30. data/lib/rubyn_code/index/prism_extractor.rb +82 -0
  31. data/lib/rubyn_code/learning/injector.rb +1 -2
  32. data/lib/rubyn_code/llm/adapters/anthropic.rb +107 -17
  33. data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +13 -0
  34. data/lib/rubyn_code/llm/adapters/base.rb +2 -1
  35. data/lib/rubyn_code/llm/adapters/openai.rb +1 -1
  36. data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +21 -0
  37. data/lib/rubyn_code/llm/client.rb +16 -3
  38. data/lib/rubyn_code/llm/image_reader.rb +60 -0
  39. data/lib/rubyn_code/llm/message_builder.rb +21 -1
  40. data/lib/rubyn_code/llm/model_router.rb +4 -4
  41. data/lib/rubyn_code/mcp/discovery.rb +93 -0
  42. data/lib/rubyn_code/memory/session_persistence.rb +1 -1
  43. data/lib/rubyn_code/observability/cost_calculator.rb +6 -3
  44. data/lib/rubyn_code/protocols/RUBYN.md +0 -3
  45. data/lib/rubyn_code/tasks/models.rb +0 -16
  46. data/lib/rubyn_code/teams/teammate.rb +0 -15
  47. data/lib/rubyn_code/tools/RUBYN.md +3 -3
  48. data/lib/rubyn_code/tools/bash.rb +3 -3
  49. data/lib/rubyn_code/tools/code_graph.rb +134 -0
  50. data/lib/rubyn_code/tools/executor.rb +6 -1
  51. data/lib/rubyn_code/tools/phone_a_friend.rb +135 -0
  52. data/lib/rubyn_code/tools/todo_store.rb +55 -0
  53. data/lib/rubyn_code/tools/todo_write.rb +88 -0
  54. data/lib/rubyn_code/version.rb +1 -1
  55. data/lib/rubyn_code.rb +14 -8
  56. data/skills/rubyn_self_test.md +140 -0
  57. metadata +11 -7
  58. data/lib/rubyn_code/context/context_budget.rb +0 -183
  59. data/lib/rubyn_code/context/schema_filter.rb +0 -64
  60. data/lib/rubyn_code/learning/shortcut.rb +0 -95
  61. data/lib/rubyn_code/llm/adapters/token_caching.rb +0 -54
  62. data/lib/rubyn_code/llm/streaming.rb +0 -10
  63. data/lib/rubyn_code/protocols/plan_approval.rb +0 -72
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubynCode
4
- module Context
5
- # Extracts only the relevant table definitions from db/schema.rb
6
- # based on which models are currently in context. Loading the full
7
- # schema for a large Rails app can be 5-10K tokens; filtering to
8
- # relevant tables typically reduces this to 200-500 tokens.
9
- module SchemaFilter
10
- TABLE_PATTERN = /create_table\s+"([^"]+)"/
11
- END_PATTERN = /\A\s+end\s*\z/
12
-
13
- class << self
14
- # Returns schema definitions for only the specified table names.
15
- #
16
- # @param schema_path [String] path to db/schema.rb
17
- # @param table_names [Array<String>] table names to include
18
- # @return [String] filtered schema content
19
- def filter(schema_path, table_names:)
20
- return '' if table_names.empty?
21
- return '' unless File.exist?(schema_path)
22
-
23
- lines = File.readlines(schema_path)
24
- extract_tables(lines, table_names.to_set(&:to_s))
25
- end
26
-
27
- # Derives table names from model class names using Rails conventions.
28
- #
29
- # @param model_names [Array<String>] e.g., ["User", "OrderItem"]
30
- # @return [Array<String>] e.g., ["users", "order_items"]
31
- def tableize(model_names)
32
- model_names.map { |name| "#{name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase}s" }
33
- end
34
-
35
- # Convenience: filter schema by model names instead of table names.
36
- def filter_for_models(schema_path, model_names:)
37
- tables = tableize(model_names)
38
- filter(schema_path, table_names: tables)
39
- end
40
-
41
- private
42
-
43
- def extract_tables(lines, table_set)
44
- result = []
45
- capturing = false
46
-
47
- lines.each do |line|
48
- match = TABLE_PATTERN.match(line)
49
- capturing = true if match && table_set.include?(match[1])
50
-
51
- result << line if capturing
52
-
53
- if capturing && END_PATTERN.match?(line)
54
- capturing = false
55
- result << "\n"
56
- end
57
- end
58
-
59
- result.join
60
- end
61
- end
62
- end
63
- end
64
- end
@@ -1,95 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubynCode
4
- module Learning
5
- # Uses learned instincts to skip redundant discovery steps. For example,
6
- # if we know the project uses FactoryBot, skip checking test_helper.rb
7
- # and looking for factories/ — just generate FactoryBot-style specs.
8
- class Shortcut
9
- SHORTCUT_RULES = {
10
- 'uses_factory_bot' => {
11
- skip: %w[test_helper factories_check],
12
- apply: { spec_template: :factory_bot_rspec }
13
- },
14
- 'uses_rspec' => {
15
- skip: %w[framework_detection],
16
- apply: { test_framework: :rspec }
17
- },
18
- 'uses_minitest' => {
19
- skip: %w[framework_detection],
20
- apply: { test_framework: :minitest }
21
- },
22
- 'uses_service_objects' => {
23
- skip: %w[pattern_detection],
24
- apply: { service_pattern: 'app/services/**/*_service.rb' }
25
- },
26
- 'uses_devise' => {
27
- skip: %w[auth_detection],
28
- apply: { auth_framework: :devise }
29
- },
30
- 'uses_grape' => {
31
- skip: %w[api_detection],
32
- apply: { api_framework: :grape }
33
- },
34
- 'uses_sidekiq' => {
35
- skip: %w[job_detection],
36
- apply: { job_framework: :sidekiq }
37
- }
38
- }.freeze
39
-
40
- attr_reader :applied_shortcuts, :tokens_saved_estimate
41
-
42
- def initialize
43
- @applied_shortcuts = []
44
- @tokens_saved_estimate = 0
45
- end
46
-
47
- # Apply shortcuts based on instinct patterns.
48
- #
49
- # @param instinct_patterns [Array<String>] patterns from the instincts table
50
- # @return [Hash] aggregated settings from applied shortcuts
51
- def apply(instinct_patterns)
52
- settings = {}
53
-
54
- instinct_patterns.each do |pattern|
55
- rule_key = match_rule(pattern)
56
- next unless rule_key
57
-
58
- rule = SHORTCUT_RULES[rule_key]
59
- settings.merge!(rule[:apply])
60
- @applied_shortcuts << { rule: rule_key, skipped: rule[:skip] }
61
- @tokens_saved_estimate += rule[:skip].size * 500
62
- end
63
-
64
- settings
65
- end
66
-
67
- # Check if a discovery step should be skipped.
68
- #
69
- # @param step_name [String] the discovery step name
70
- # @return [Boolean]
71
- def skip?(step_name)
72
- @applied_shortcuts.any? { |s| s[:skipped].include?(step_name.to_s) }
73
- end
74
-
75
- # Returns stats about shortcuts applied this session.
76
- def stats
77
- {
78
- shortcuts_applied: @applied_shortcuts.size,
79
- steps_skipped: @applied_shortcuts.sum { |s| s[:skipped].size },
80
- tokens_saved_estimate: @tokens_saved_estimate
81
- }
82
- end
83
-
84
- private
85
-
86
- def match_rule(pattern)
87
- normalized = pattern.to_s.downcase
88
- SHORTCUT_RULES.each_key do |key|
89
- return key if normalized.include?(key.tr('_', ' ')) || normalized.include?(key)
90
- end
91
- nil
92
- end
93
- end
94
- end
95
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubynCode
4
- module LLM
5
- module Adapters
6
- # Cached access to the Anthropic credential.
7
- #
8
- # TokenStore.load shells out to the macOS keychain, so the loaded
9
- # tokens are cached per adapter instance. The cache drops itself once
10
- # the token nears expiry (picking up an externally refreshed
11
- # credential) and must be invalidated on 401 responses.
12
- module TokenCaching
13
- private
14
-
15
- def oauth_token?
16
- return @oauth_token unless @oauth_token.nil?
17
-
18
- @oauth_token = access_token.include?('sk-ant-oat')
19
- end
20
-
21
- def ensure_valid_token!
22
- return if Auth::TokenStore.valid_tokens?(tokens)
23
-
24
- raise Client::AuthExpiredError,
25
- 'No valid authentication. Run `rubyn-code --auth` or set ANTHROPIC_API_KEY.'
26
- end
27
-
28
- def access_token
29
- token = tokens&.fetch(:access_token, nil)
30
- raise Client::AuthExpiredError, 'No stored access token' unless token
31
-
32
- token
33
- end
34
-
35
- def tokens
36
- invalidate_token_cache! if token_cache_stale?
37
- @tokens ||= Auth::TokenStore.load
38
- end
39
-
40
- def token_cache_stale?
41
- expires_at = @tokens&.fetch(:expires_at, nil)
42
- return false unless expires_at
43
-
44
- expires_at <= Time.now + Auth::TokenStore::EXPIRY_BUFFER_SECONDS
45
- end
46
-
47
- def invalidate_token_cache!
48
- @tokens = nil
49
- @oauth_token = nil
50
- end
51
- end
52
- end
53
- end
54
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubynCode
4
- module LLM
5
- # Backward-compatibility shim.
6
- # Delegates to Adapters::AnthropicStreaming so existing references
7
- # to LLM::Streaming keep working during the migration.
8
- Streaming = Adapters::AnthropicStreaming
9
- end
10
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'tty-prompt'
4
- require 'tty-reader'
5
- require 'pastel'
6
-
7
- module RubynCode
8
- module Protocols
9
- # Presents a plan to the user for approval before executing significant changes.
10
- #
11
- # This protocol ensures that destructive or wide-reaching operations are
12
- # reviewed by a human before proceeding.
13
- module PlanApproval
14
- APPROVED = :approved
15
- REJECTED = :rejected
16
-
17
- class << self
18
- # Displays a plan and asks the user to approve or reject it.
19
- #
20
- # @param plan_text [String] the plan description to display
21
- # @param prompt [String, nil] optional custom prompt message
22
- # @return [Symbol] :approved or :rejected
23
- def request(plan_text, prompt: nil)
24
- pastel = Pastel.new
25
- display_plan(pastel, plan_text, prompt)
26
-
27
- approved = build_prompt.yes?(
28
- pastel.yellow.bold('Do you approve this plan?'),
29
- default: false
30
- )
31
-
32
- approved ? approve(pastel) : reject(pastel)
33
- rescue TTY::Reader::InputInterrupt
34
- $stdout.puts pastel.red("\nPlan rejected (interrupted).")
35
- REJECTED
36
- end
37
-
38
- private
39
-
40
- def display_plan(pastel, plan_text, prompt)
41
- $stdout.puts
42
- $stdout.puts pastel.cyan.bold('Proposed Plan')
43
- $stdout.puts pastel.cyan('=' * 60)
44
- $stdout.puts plan_text
45
- $stdout.puts pastel.cyan('=' * 60)
46
- $stdout.puts
47
- return unless prompt
48
-
49
- $stdout.puts pastel.yellow(prompt)
50
- $stdout.puts
51
- end
52
-
53
- def approve(pastel)
54
- $stdout.puts pastel.green('Plan approved.')
55
- APPROVED
56
- end
57
-
58
- def reject(pastel)
59
- $stdout.puts pastel.red('Plan rejected.')
60
- REJECTED
61
- end
62
-
63
- # Builds a TTY::Prompt instance configured for non-destructive interrupt handling.
64
- #
65
- # @return [TTY::Prompt]
66
- def build_prompt
67
- TTY::Prompt.new(interrupt: :noop)
68
- end
69
- end
70
- end
71
- end
72
- end