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.
- checksums.yaml +4 -4
- data/README.md +45 -17
- data/lib/rubyn_code/agent/conversation.rb +11 -1
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +1 -1
- data/lib/rubyn_code/agent/llm_caller.rb +5 -1
- data/lib/rubyn_code/agent/loop.rb +57 -3
- data/lib/rubyn_code/agent/response_parser.rb +8 -0
- data/lib/rubyn_code/agent/system_prompt_builder.rb +3 -0
- data/lib/rubyn_code/agent/tool_processor.rb +10 -0
- data/lib/rubyn_code/autonomous/daemon.rb +1 -1
- data/lib/rubyn_code/cli/commands/context.rb +27 -0
- data/lib/rubyn_code/cli/commands/custom_command.rb +44 -2
- data/lib/rubyn_code/cli/commands/custom_loader.rb +36 -5
- data/lib/rubyn_code/cli/commands/effort.rb +47 -0
- data/lib/rubyn_code/cli/commands/export.rb +174 -0
- data/lib/rubyn_code/cli/commands/mcp.rb +32 -8
- data/lib/rubyn_code/cli/commands/resume.rb +97 -26
- data/lib/rubyn_code/cli/commands/think.rb +47 -0
- data/lib/rubyn_code/cli/first_run.rb +1 -1
- data/lib/rubyn_code/cli/mention_expander.rb +19 -0
- data/lib/rubyn_code/cli/repl.rb +31 -1
- data/lib/rubyn_code/cli/repl_commands.rb +1 -1
- data/lib/rubyn_code/cli/repl_setup.rb +8 -6
- data/lib/rubyn_code/config/defaults.rb +2 -1
- data/lib/rubyn_code/config/schema.json +5 -0
- data/lib/rubyn_code/config/settings.rb +4 -2
- data/lib/rubyn_code/context/auto_compact.rb +1 -1
- data/lib/rubyn_code/context/manual_compact.rb +1 -1
- data/lib/rubyn_code/index/codebase_index.rb +64 -3
- data/lib/rubyn_code/index/prism_extractor.rb +82 -0
- data/lib/rubyn_code/learning/injector.rb +1 -2
- data/lib/rubyn_code/llm/adapters/anthropic.rb +107 -17
- data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +13 -0
- data/lib/rubyn_code/llm/adapters/base.rb +2 -1
- data/lib/rubyn_code/llm/adapters/openai.rb +1 -1
- data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +21 -0
- data/lib/rubyn_code/llm/client.rb +16 -3
- data/lib/rubyn_code/llm/image_reader.rb +60 -0
- data/lib/rubyn_code/llm/message_builder.rb +21 -1
- data/lib/rubyn_code/llm/model_router.rb +4 -4
- data/lib/rubyn_code/mcp/discovery.rb +93 -0
- data/lib/rubyn_code/memory/session_persistence.rb +1 -1
- data/lib/rubyn_code/observability/cost_calculator.rb +6 -3
- data/lib/rubyn_code/protocols/RUBYN.md +0 -3
- data/lib/rubyn_code/tasks/models.rb +0 -16
- data/lib/rubyn_code/teams/teammate.rb +0 -15
- data/lib/rubyn_code/tools/RUBYN.md +3 -3
- data/lib/rubyn_code/tools/bash.rb +3 -3
- data/lib/rubyn_code/tools/code_graph.rb +134 -0
- data/lib/rubyn_code/tools/executor.rb +6 -1
- data/lib/rubyn_code/tools/phone_a_friend.rb +135 -0
- data/lib/rubyn_code/tools/todo_store.rb +55 -0
- data/lib/rubyn_code/tools/todo_write.rb +88 -0
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +14 -8
- data/skills/rubyn_self_test.md +140 -0
- metadata +11 -7
- data/lib/rubyn_code/context/context_budget.rb +0 -183
- data/lib/rubyn_code/context/schema_filter.rb +0 -64
- data/lib/rubyn_code/learning/shortcut.rb +0 -95
- data/lib/rubyn_code/llm/adapters/token_caching.rb +0 -54
- data/lib/rubyn_code/llm/streaming.rb +0 -10
- 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
|