rubyn-code 0.4.0 → 0.5.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 +186 -2
- data/lib/rubyn_code/agent/conversation.rb +2 -1
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +2 -1
- data/lib/rubyn_code/agent/llm_caller.rb +4 -2
- data/lib/rubyn_code/agent/loop.rb +7 -3
- data/lib/rubyn_code/agent/response_modes.rb +2 -1
- data/lib/rubyn_code/agent/system_prompt_builder.rb +39 -0
- data/lib/rubyn_code/agent/tool_processor.rb +4 -2
- data/lib/rubyn_code/cli/app.rb +85 -11
- data/lib/rubyn_code/cli/commands/install_skills.rb +44 -0
- data/lib/rubyn_code/cli/commands/list_skills.rb +149 -0
- data/lib/rubyn_code/cli/commands/provider.rb +2 -1
- data/lib/rubyn_code/cli/commands/remove_skills.rb +35 -0
- data/lib/rubyn_code/cli/commands/skill.rb +4 -2
- data/lib/rubyn_code/cli/commands/skills.rb +104 -0
- data/lib/rubyn_code/cli/repl.rb +11 -1
- data/lib/rubyn_code/cli/repl_commands.rb +2 -1
- data/lib/rubyn_code/cli/repl_setup.rb +38 -1
- data/lib/rubyn_code/config/defaults.rb +2 -0
- data/lib/rubyn_code/config/settings.rb +5 -2
- data/lib/rubyn_code/context/context_budget.rb +2 -1
- data/lib/rubyn_code/context/manager.rb +3 -3
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +6 -3
- data/lib/rubyn_code/ide/handlers/review_handler.rb +19 -2
- data/lib/rubyn_code/ide/protocol.rb +2 -1
- data/lib/rubyn_code/index/codebase_index.rb +2 -1
- data/lib/rubyn_code/learning/extractor.rb +4 -2
- data/lib/rubyn_code/llm/model_router.rb +2 -1
- data/lib/rubyn_code/mcp/tool_bridge.rb +1 -1
- data/lib/rubyn_code/observability/usage_reporter.rb +4 -2
- data/lib/rubyn_code/output/diff_renderer.rb +3 -2
- data/lib/rubyn_code/self_test.rb +2 -1
- data/lib/rubyn_code/skills/auto_suggest.rb +131 -0
- data/lib/rubyn_code/skills/catalog.rb +10 -0
- data/lib/rubyn_code/skills/document.rb +8 -2
- data/lib/rubyn_code/skills/gemfile_parser.rb +40 -0
- data/lib/rubyn_code/skills/loader.rb +1 -1
- data/lib/rubyn_code/skills/matcher.rb +89 -0
- data/lib/rubyn_code/skills/pack_context.rb +163 -0
- data/lib/rubyn_code/skills/pack_installer.rb +194 -0
- data/lib/rubyn_code/skills/pack_manager.rb +230 -0
- data/lib/rubyn_code/skills/registry_autoload.rb +112 -0
- data/lib/rubyn_code/skills/registry_client.rb +241 -0
- data/lib/rubyn_code/tools/executor.rb +4 -2
- data/lib/rubyn_code/tools/grep.rb +2 -1
- data/lib/rubyn_code/tools/ide_diagnostics.rb +3 -1
- data/lib/rubyn_code/tools/ide_symbols.rb +3 -1
- data/lib/rubyn_code/tools/load_skill.rb +2 -1
- data/lib/rubyn_code/tools/output_compressor.rb +3 -6
- data/lib/rubyn_code/tools/review_pr.rb +15 -4
- data/lib/rubyn_code/tools/web_search.rb +2 -1
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +12 -0
- data/skills/rubyn_self_test.md +75 -0
- metadata +13 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class ListSkills < Base
|
|
7
|
+
def self.command_name = '/skills'
|
|
8
|
+
def self.description = 'List installed skills or browse the registry'
|
|
9
|
+
|
|
10
|
+
def execute(args, ctx)
|
|
11
|
+
if args.include?('--available')
|
|
12
|
+
list_available(ctx)
|
|
13
|
+
else
|
|
14
|
+
list_installed(ctx)
|
|
15
|
+
end
|
|
16
|
+
rescue Skills::RegistryError => e
|
|
17
|
+
ctx.renderer.error("Registry error: #{e.message}")
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
ctx.renderer.error("Skills error: #{e.message}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def list_installed(ctx)
|
|
25
|
+
catalog = ctx.skill_loader.catalog
|
|
26
|
+
all_skills = catalog.available
|
|
27
|
+
buckets = partition_skills(all_skills, ctx)
|
|
28
|
+
|
|
29
|
+
ctx.renderer.info("Loaded skills (#{all_skills.size} total)")
|
|
30
|
+
puts
|
|
31
|
+
|
|
32
|
+
render_builtin(buckets[:builtin], catalog)
|
|
33
|
+
render_packs(buckets[:packs])
|
|
34
|
+
render_community(buckets[:community], buckets[:packs].any?)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def partition_skills(all_skills, ctx)
|
|
38
|
+
community_dir = File.join(ctx.project_root, '.rubyn-code', 'skills')
|
|
39
|
+
global_dir = File.join(Config::Defaults::HOME_DIR, 'skills')
|
|
40
|
+
pack_dir = File.join(Config::Defaults::HOME_DIR, 'skill-packs')
|
|
41
|
+
|
|
42
|
+
buckets = { builtin: [], community: [], packs: [] }
|
|
43
|
+
all_skills.each do |skill|
|
|
44
|
+
buckets[bucket_for(skill[:path].to_s, pack_dir, community_dir, global_dir)] << skill
|
|
45
|
+
end
|
|
46
|
+
buckets
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def bucket_for(path, pack_dir, community_dir, global_dir)
|
|
50
|
+
return :packs if path.start_with?(pack_dir)
|
|
51
|
+
return :community if path.start_with?(community_dir) || path.start_with?(global_dir)
|
|
52
|
+
|
|
53
|
+
:builtin
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def render_builtin(builtin, catalog)
|
|
57
|
+
puts " Built-in (#{builtin.size})"
|
|
58
|
+
builtin.group_by { |s| category_from_path(s[:path], catalog.skills_dirs) }
|
|
59
|
+
.sort_by { |cat, _| cat }
|
|
60
|
+
.each do |cat, skills|
|
|
61
|
+
label = cat.empty? ? 'general' : cat
|
|
62
|
+
puts " #{label}: #{skills.map { |s| s[:name] }.join(', ')}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render_packs(packs)
|
|
67
|
+
return if packs.empty?
|
|
68
|
+
|
|
69
|
+
puts
|
|
70
|
+
summary = packs.group_by { |s| pack_from_path(s[:path]) }
|
|
71
|
+
.sort
|
|
72
|
+
.map { |pack, skills| "#{pack} (#{skills.size})" }
|
|
73
|
+
.join(', ')
|
|
74
|
+
puts " Skill packs: #{summary}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_community(community, packs_present)
|
|
78
|
+
if community.any?
|
|
79
|
+
puts
|
|
80
|
+
summary = community.group_by { |s| pack_from_path(s[:path]) }
|
|
81
|
+
.map { |pack, skills| "#{pack} (#{skills.size})" }
|
|
82
|
+
.join(', ')
|
|
83
|
+
puts " Community: #{summary}"
|
|
84
|
+
elsif !packs_present
|
|
85
|
+
puts
|
|
86
|
+
puts ' Skill packs: none installed'
|
|
87
|
+
puts ' Run /skills --available to browse, or /install-skills <name> to install.'
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def list_available(ctx) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- readable catalog display
|
|
92
|
+
client = Skills::RegistryClient.new
|
|
93
|
+
catalog_data = client.fetch_catalog
|
|
94
|
+
packs = catalog_data['packs'] || []
|
|
95
|
+
|
|
96
|
+
if packs.empty?
|
|
97
|
+
ctx.renderer.info('No packs available in the registry.')
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
installer = Skills::PackInstaller.new(
|
|
102
|
+
registry_client: client,
|
|
103
|
+
project_root: ctx.project_root
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
ctx.renderer.info("Available skill packs (#{packs.size})")
|
|
107
|
+
puts
|
|
108
|
+
|
|
109
|
+
# Group by category
|
|
110
|
+
by_category = packs.group_by { |p| p['category'] || 'other' }
|
|
111
|
+
by_category.sort_by { |cat, _| cat }.each do |category, cat_packs|
|
|
112
|
+
puts " #{category.capitalize}"
|
|
113
|
+
cat_packs.each do |pack|
|
|
114
|
+
installed = installer.installed?(pack['name'])
|
|
115
|
+
marker = installed ? ' ✓' : ''
|
|
116
|
+
desc = pack['description']&.slice(0, 50) || ''
|
|
117
|
+
puts " #{pack['name'].ljust(20)} #{desc} (#{pack['skillCount']} skills)#{marker}"
|
|
118
|
+
end
|
|
119
|
+
puts
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
puts ' Install with: /install-skills <name>'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def category_from_path(path, skills_dirs)
|
|
126
|
+
skills_dirs.each do |dir|
|
|
127
|
+
expanded = File.expand_path(dir)
|
|
128
|
+
next unless path.to_s.start_with?(expanded)
|
|
129
|
+
|
|
130
|
+
relative = path.delete_prefix("#{expanded}/")
|
|
131
|
+
parts = relative.split('/')
|
|
132
|
+
return parts.size > 1 ? parts.first : ''
|
|
133
|
+
end
|
|
134
|
+
''
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def pack_from_path(path)
|
|
138
|
+
# Pack-style layouts: .rubyn-code/skills/<pack>/<file>.md
|
|
139
|
+
# or .rubyn-code/skill-packs/<pack>/<file>.md
|
|
140
|
+
parts = path.to_s.split('/')
|
|
141
|
+
idx = parts.rindex('skill-packs') || parts.rindex('skills')
|
|
142
|
+
return 'unknown' unless idx
|
|
143
|
+
|
|
144
|
+
parts[idx + 1] || 'unknown'
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -55,7 +55,8 @@ module RubynCode
|
|
|
55
55
|
Auth::TokenStore.save_provider_key(name, opts[:key]) if opts[:key]
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
# -- sequential output lines
|
|
59
|
+
def confirm_added(name, base_url, opts, ctx)
|
|
59
60
|
ctx.renderer.success("Provider '#{name}' added (#{opts[:api_format] || 'openai'} format)")
|
|
60
61
|
ctx.renderer.info(" base_url: #{base_url}")
|
|
61
62
|
ctx.renderer.info(' api_key: stored') if opts[:key]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class RemoveSkills < Base
|
|
7
|
+
def self.command_name = '/remove-skills'
|
|
8
|
+
def self.description = 'Remove installed skill packs'
|
|
9
|
+
|
|
10
|
+
def execute(args, ctx)
|
|
11
|
+
if args.empty?
|
|
12
|
+
ctx.renderer.warning('Usage: /remove-skills <pack-name> [pack-name ...]')
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
pack_manager = RubynCode::Skills::PackManager.new
|
|
17
|
+
|
|
18
|
+
args.each { |name| remove_pack(name, pack_manager, ctx) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def remove_pack(name, pack_manager, ctx)
|
|
24
|
+
unless pack_manager.installed?(name)
|
|
25
|
+
ctx.renderer.warning("Pack '#{name}' is not installed.")
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
pack_manager.remove(name)
|
|
30
|
+
ctx.renderer.info("Removed skill pack '#{name}'.")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -33,7 +33,8 @@ module RubynCode
|
|
|
33
33
|
skills.each { |skill| puts " /#{skill[:name]}: #{skill[:description]}" }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
# -- readable sequential display logic
|
|
37
|
+
def search_skills(term, ctx)
|
|
37
38
|
if term.nil? || term.strip.empty?
|
|
38
39
|
ctx.renderer.warning('Usage: /skill search <term>')
|
|
39
40
|
return
|
|
@@ -49,7 +50,8 @@ module RubynCode
|
|
|
49
50
|
display_entries(results)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
# -- readable sequential display logic
|
|
54
|
+
def list_by_category(category, ctx)
|
|
53
55
|
catalog = ctx.skill_loader.catalog
|
|
54
56
|
return list_categories(catalog, ctx) if category.nil? || category.strip.empty?
|
|
55
57
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class Skills < Base
|
|
7
|
+
def self.command_name = '/skills'
|
|
8
|
+
def self.description = 'List installed skill packs or browse the registry'
|
|
9
|
+
|
|
10
|
+
def execute(args, ctx)
|
|
11
|
+
case args.first
|
|
12
|
+
when 'search' then search_registry(args[1..].join(' '), ctx)
|
|
13
|
+
when 'available' then list_available(ctx)
|
|
14
|
+
when nil, 'list' then list_installed(ctx)
|
|
15
|
+
else
|
|
16
|
+
ctx.renderer.warning(
|
|
17
|
+
"Unknown subcommand '#{args.first}'. Try: /skills, /skills available, /skills search <term>"
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
rescue RubynCode::Skills::RegistryError => e
|
|
21
|
+
ctx.renderer.error(e.message)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def list_installed(ctx)
|
|
27
|
+
packs = RubynCode::Skills::PackManager.new.installed
|
|
28
|
+
|
|
29
|
+
if packs.empty?
|
|
30
|
+
ctx.renderer.info(
|
|
31
|
+
'No skill packs installed. Use /skills available to browse or /install-skills <name> to install.'
|
|
32
|
+
)
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
ctx.renderer.info("Installed skill packs (#{packs.size}):")
|
|
37
|
+
packs.each { |pack| puts " #{format_installed_pack(pack)}" }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def list_available(ctx)
|
|
41
|
+
ctx.renderer.info('Fetching available packs from registry...')
|
|
42
|
+
result = RubynCode::Skills::RegistryClient.new.fetch_catalog
|
|
43
|
+
packs = result[:data]
|
|
44
|
+
return ctx.renderer.info('No packs found in the registry.') unless valid_results?(packs)
|
|
45
|
+
|
|
46
|
+
pack_manager = RubynCode::Skills::PackManager.new
|
|
47
|
+
ctx.renderer.info("Available skill packs (#{packs.size}):")
|
|
48
|
+
packs.each { |pack| puts " #{format_available_pack(pack, pack_manager)}" }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def search_registry(term, ctx)
|
|
52
|
+
if term.nil? || term.strip.empty?
|
|
53
|
+
ctx.renderer.warning('Usage: /skills search <term>')
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
query = term.strip
|
|
58
|
+
ctx.renderer.info("Searching registry for '#{query}'...")
|
|
59
|
+
result = RubynCode::Skills::RegistryClient.new.search_packs(query)
|
|
60
|
+
packs = result[:data]
|
|
61
|
+
|
|
62
|
+
unless valid_results?(packs)
|
|
63
|
+
ctx.renderer.info("No packs found matching '#{query}'.")
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ctx.renderer.info("Packs matching '#{query}' (#{packs.size}):")
|
|
68
|
+
packs.each { |pack| puts " #{format_pack_line(pack)}" }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def valid_results?(results)
|
|
72
|
+
results.is_a?(Array) && !results.empty?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def format_installed_pack(pack)
|
|
76
|
+
version = pack[:version] ? " v#{pack[:version]}" : ''
|
|
77
|
+
desc = pack[:description].to_s.empty? ? '' : " — #{pack[:description]}"
|
|
78
|
+
"#{pack[:name]}#{version}#{desc}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def format_available_pack(pack, pack_manager)
|
|
82
|
+
name = pack_name(pack)
|
|
83
|
+
installed = pack_manager.installed?(name) ? ' [installed]' : ''
|
|
84
|
+
"#{format_pack_line(pack)}#{installed}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def format_pack_line(pack)
|
|
88
|
+
name = pack_name(pack)
|
|
89
|
+
desc = pack_description(pack)
|
|
90
|
+
label = desc.empty? ? '' : " — #{desc}"
|
|
91
|
+
"#{name}#{label}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def pack_name(pack)
|
|
95
|
+
pack[:name] || pack['name']
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def pack_description(pack)
|
|
99
|
+
(pack[:description] || pack['description']).to_s
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/rubyn_code/cli/repl.rb
CHANGED
|
@@ -34,6 +34,7 @@ module RubynCode
|
|
|
34
34
|
|
|
35
35
|
@renderer.welcome
|
|
36
36
|
@version_check.notify
|
|
37
|
+
check_skill_suggestions!
|
|
37
38
|
|
|
38
39
|
at_exit { shutdown! }
|
|
39
40
|
|
|
@@ -44,6 +45,14 @@ module RubynCode
|
|
|
44
45
|
|
|
45
46
|
private
|
|
46
47
|
|
|
48
|
+
def check_skill_suggestions!
|
|
49
|
+
suggest = Skills::AutoSuggest.new(project_root: @project_root)
|
|
50
|
+
message = suggest.check
|
|
51
|
+
@renderer.info(message) if message
|
|
52
|
+
rescue StandardError
|
|
53
|
+
# Never block session start on suggestion failure
|
|
54
|
+
end
|
|
55
|
+
|
|
47
56
|
def run_input_loop
|
|
48
57
|
while @running
|
|
49
58
|
begin
|
|
@@ -98,7 +107,8 @@ module RubynCode
|
|
|
98
107
|
@stream_formatter&.feed(text)
|
|
99
108
|
end
|
|
100
109
|
|
|
101
|
-
|
|
110
|
+
# -- sequential steps with interrupt rescue
|
|
111
|
+
def handle_message(input)
|
|
102
112
|
@spinner.start
|
|
103
113
|
@streaming_first_chunk = true
|
|
104
114
|
|
|
@@ -22,7 +22,8 @@ module RubynCode
|
|
|
22
22
|
Commands::Spawn, Commands::Doctor, Commands::Tokens,
|
|
23
23
|
Commands::Plan, Commands::ContextInfo, Commands::Diff,
|
|
24
24
|
Commands::Model, Commands::NewSession, Commands::Mcp,
|
|
25
|
-
Commands::Provider
|
|
25
|
+
Commands::Provider, Commands::InstallSkills,
|
|
26
|
+
Commands::RemoveSkills, Commands::Skills
|
|
26
27
|
].each { |cmd| @command_registry.register(cmd) }
|
|
27
28
|
end
|
|
28
29
|
|
|
@@ -42,9 +42,41 @@ module RubynCode
|
|
|
42
42
|
@budget_enforcer = Observability::BudgetEnforcer.new(@db, session_id: current_session_id)
|
|
43
43
|
@background_worker = Background::Worker.new(project_root: @project_root)
|
|
44
44
|
@skill_loader = Skills::Loader.new(Skills::Catalog.new(skill_dirs))
|
|
45
|
+
@skill_matcher = build_skill_matcher
|
|
46
|
+
@web_skill_autoload = build_web_skill_autoload
|
|
45
47
|
@session_persistence = Memory::SessionPersistence.new(@db)
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
def build_skill_matcher
|
|
51
|
+
return nil unless skills_autoload_enabled?
|
|
52
|
+
|
|
53
|
+
Skills::Matcher.new(catalog: @skill_loader.catalog, project_root: @project_root)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_web_skill_autoload
|
|
57
|
+
return nil unless @skill_matcher && skills_autoload_enabled?
|
|
58
|
+
|
|
59
|
+
Skills::RegistryAutoload.new(
|
|
60
|
+
loader: @skill_loader,
|
|
61
|
+
matcher: @skill_matcher,
|
|
62
|
+
on_fetching: on_pack_fetching_callback
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def skills_autoload_enabled?
|
|
67
|
+
Config::Settings.new.skills_autoload
|
|
68
|
+
rescue Config::Settings::LoadError
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def on_skills_autoloaded_callback
|
|
73
|
+
->(names) { @renderer.system_message("📚 Loaded: #{names.join(' · ')}") }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def on_pack_fetching_callback
|
|
77
|
+
->(name) { @renderer.system_message("📥 Fetching skill pack '#{name}' from registry…") }
|
|
78
|
+
end
|
|
79
|
+
|
|
48
80
|
def setup_executor_callbacks!
|
|
49
81
|
@tool_executor.llm_client = @llm_client
|
|
50
82
|
@tool_executor.background_worker = @background_worker
|
|
@@ -107,7 +139,10 @@ module RubynCode
|
|
|
107
139
|
on_tool_call: ->(name, params) { handle_on_tool_call(name, params) },
|
|
108
140
|
on_tool_result: ->(name, result, _is_error = false) { handle_on_tool_result(name, result) },
|
|
109
141
|
on_text: ->(text) { handle_on_text(text) },
|
|
110
|
-
|
|
142
|
+
on_skills_autoloaded: on_skills_autoloaded_callback,
|
|
143
|
+
skill_loader: @skill_loader, skill_matcher: @skill_matcher,
|
|
144
|
+
web_skill_autoload: @web_skill_autoload,
|
|
145
|
+
project_root: @project_root
|
|
111
146
|
)
|
|
112
147
|
end
|
|
113
148
|
|
|
@@ -139,6 +174,8 @@ module RubynCode
|
|
|
139
174
|
dirs << project_skills if Dir.exist?(project_skills)
|
|
140
175
|
user_skills = File.join(Config::Defaults::HOME_DIR, 'skills')
|
|
141
176
|
dirs << user_skills if Dir.exist?(user_skills)
|
|
177
|
+
skill_packs = File.join(Config::Defaults::HOME_DIR, 'skill-packs')
|
|
178
|
+
dirs << skill_packs if Dir.exist?(skill_packs)
|
|
142
179
|
dirs
|
|
143
180
|
end
|
|
144
181
|
|
|
@@ -16,6 +16,7 @@ module RubynCode
|
|
|
16
16
|
session_budget_usd daily_budget_usd
|
|
17
17
|
oauth_client_id oauth_redirect_uri oauth_authorize_url
|
|
18
18
|
oauth_token_url oauth_scopes
|
|
19
|
+
skills_autoload
|
|
19
20
|
].freeze
|
|
20
21
|
|
|
21
22
|
DEFAULT_MAP = {
|
|
@@ -35,7 +36,8 @@ module RubynCode
|
|
|
35
36
|
oauth_redirect_uri: Defaults::OAUTH_REDIRECT_URI,
|
|
36
37
|
oauth_authorize_url: Defaults::OAUTH_AUTHORIZE_URL,
|
|
37
38
|
oauth_token_url: Defaults::OAUTH_TOKEN_URL,
|
|
38
|
-
oauth_scopes: Defaults::OAUTH_SCOPES
|
|
39
|
+
oauth_scopes: Defaults::OAUTH_SCOPES,
|
|
40
|
+
skills_autoload: Defaults::SKILLS_AUTOLOAD
|
|
39
41
|
}.freeze
|
|
40
42
|
|
|
41
43
|
attr_reader :config_path, :data
|
|
@@ -160,7 +162,8 @@ module RubynCode
|
|
|
160
162
|
|
|
161
163
|
# Backfills missing 'models' keys into existing provider configs.
|
|
162
164
|
# Never overwrites user-set values — only adds what's missing.
|
|
163
|
-
|
|
165
|
+
# -- iterates providers with guard clauses
|
|
166
|
+
def backfill_provider_models!
|
|
164
167
|
providers = @data['providers']
|
|
165
168
|
return unless providers.is_a?(Hash)
|
|
166
169
|
|
|
@@ -130,7 +130,8 @@ module RubynCode
|
|
|
130
130
|
end
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
# -- signature extraction dispatch
|
|
134
|
+
def process_signature_line(line, signatures, indent_stack)
|
|
134
135
|
stripped = line.strip
|
|
135
136
|
if signature_line?(stripped)
|
|
136
137
|
signatures << line
|
|
@@ -74,11 +74,9 @@ module RubynCode
|
|
|
74
74
|
MICRO_COMPACT_RATIO_UNCACHED = 0.5
|
|
75
75
|
|
|
76
76
|
def check_compaction!(conversation)
|
|
77
|
-
# Guard: skip if compaction already
|
|
77
|
+
# Guard: skip if compaction already succeeded this turn
|
|
78
78
|
return if @last_compaction_turn == @current_turn
|
|
79
79
|
|
|
80
|
-
@last_compaction_turn = @current_turn
|
|
81
|
-
|
|
82
80
|
messages = conversation.messages
|
|
83
81
|
|
|
84
82
|
# Step 1: Zero-cost micro-compact — but only when we're approaching
|
|
@@ -93,6 +91,7 @@ module RubynCode
|
|
|
93
91
|
collapsed = ContextCollapse.call(messages, threshold: @threshold)
|
|
94
92
|
if collapsed
|
|
95
93
|
apply_compacted_messages(conversation, collapsed)
|
|
94
|
+
@last_compaction_turn = @current_turn
|
|
96
95
|
return
|
|
97
96
|
end
|
|
98
97
|
|
|
@@ -102,6 +101,7 @@ module RubynCode
|
|
|
102
101
|
compactor = Compactor.new(llm_client: @llm_client, threshold: @threshold)
|
|
103
102
|
new_messages = compactor.auto_compact!(messages)
|
|
104
103
|
apply_compacted_messages(conversation, new_messages)
|
|
104
|
+
@last_compaction_turn = @current_turn
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
# Resets cumulative token counters to zero.
|
|
@@ -62,7 +62,8 @@ module RubynCode
|
|
|
62
62
|
|
|
63
63
|
private
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
# -- orchestrates agent lifecycle with notifications
|
|
66
|
+
def run_agent(session_id, text, context)
|
|
66
67
|
@server.notify('agent/status', {
|
|
67
68
|
'sessionId' => session_id,
|
|
68
69
|
'status' => 'thinking'
|
|
@@ -165,7 +166,8 @@ module RubynCode
|
|
|
165
166
|
# Install a ToolOutput adapter on the server so AcceptEdit /
|
|
166
167
|
# ApproveToolUse handlers can route responses back to this session.
|
|
167
168
|
def build_tool_output_adapter
|
|
168
|
-
adapter = IDE::Adapters::ToolOutput.new(@server, permission_mode: @server.permission_mode,
|
|
169
|
+
adapter = IDE::Adapters::ToolOutput.new(@server, permission_mode: @server.permission_mode,
|
|
170
|
+
hook_runner: build_ide_hook_runner)
|
|
169
171
|
@server.tool_output_adapter = adapter
|
|
170
172
|
adapter
|
|
171
173
|
end
|
|
@@ -190,7 +192,8 @@ module RubynCode
|
|
|
190
192
|
Hooks::Runner.new(registry: registry)
|
|
191
193
|
end
|
|
192
194
|
|
|
193
|
-
|
|
195
|
+
# -- assembles context parts from multiple optional fields
|
|
196
|
+
def build_enriched_input(text, context)
|
|
194
197
|
parts = []
|
|
195
198
|
|
|
196
199
|
parts << "[Active file: #{context['activeFile']}]" if context['activeFile']
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../../skills/pack_context'
|
|
4
|
+
|
|
3
5
|
module RubynCode
|
|
4
6
|
module IDE
|
|
5
7
|
module Handlers
|
|
@@ -31,15 +33,16 @@ module RubynCode
|
|
|
31
33
|
|
|
32
34
|
private
|
|
33
35
|
|
|
34
|
-
def run_review(session_id, base_branch, focus) # rubocop:disable Metrics/
|
|
36
|
+
def run_review(session_id, base_branch, focus) # rubocop:disable Metrics/MethodLength -- review lifecycle with finding notifications
|
|
35
37
|
@server.notify('agent/status', {
|
|
36
38
|
'sessionId' => session_id,
|
|
37
39
|
'status' => 'reviewing'
|
|
38
40
|
})
|
|
39
41
|
|
|
40
42
|
workspace = @server.workspace_path || Dir.pwd
|
|
43
|
+
pack_context = build_pack_context(workspace)
|
|
41
44
|
review_tool = Tools::ReviewPr.new(project_root: workspace)
|
|
42
|
-
result = review_tool.execute(base_branch: base_branch, focus: focus)
|
|
45
|
+
result = review_tool.execute(base_branch: base_branch, focus: focus, pack_context: pack_context)
|
|
43
46
|
|
|
44
47
|
# Parse the review output into individual findings and emit them
|
|
45
48
|
findings = extract_findings(result)
|
|
@@ -68,6 +71,20 @@ module RubynCode
|
|
|
68
71
|
})
|
|
69
72
|
end
|
|
70
73
|
|
|
74
|
+
# Fetch skill pack context for gems detected in the repo's Gemfile.
|
|
75
|
+
# Returns nil on any failure — pack context is best-effort and must never
|
|
76
|
+
# block the review from running.
|
|
77
|
+
#
|
|
78
|
+
# @param workspace [String] absolute path to the repository
|
|
79
|
+
# @return [String, nil] formatted context block or nil
|
|
80
|
+
def build_pack_context(workspace)
|
|
81
|
+
context = Skills::PackContext.for_repo(project_root: workspace)
|
|
82
|
+
block = context.build_context_block
|
|
83
|
+
block.empty? ? nil : block
|
|
84
|
+
rescue StandardError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
71
88
|
def extract_findings(review_text)
|
|
72
89
|
return [] unless review_text.is_a?(String)
|
|
73
90
|
|
|
@@ -25,7 +25,8 @@ module RubynCode
|
|
|
25
25
|
|
|
26
26
|
# Parse a JSON string into a request hash.
|
|
27
27
|
# Returns either a valid request hash or an error response hash.
|
|
28
|
-
|
|
28
|
+
# -- JSON-RPC validation checks
|
|
29
|
+
def parse(line)
|
|
29
30
|
begin
|
|
30
31
|
data = JSON.parse(line)
|
|
31
32
|
rescue JSON::ParserError
|
|
@@ -260,7 +260,8 @@ module RubynCode
|
|
|
260
260
|
end
|
|
261
261
|
end
|
|
262
262
|
|
|
263
|
-
|
|
263
|
+
# -- Rails directory mapping
|
|
264
|
+
def classify_node(file, type)
|
|
264
265
|
return 'model' if file.include?('app/models/')
|
|
265
266
|
return 'controller' if file.include?('app/controllers/')
|
|
266
267
|
return 'service' if file.include?('app/services/')
|
|
@@ -82,7 +82,8 @@ module RubynCode
|
|
|
82
82
|
messages.map { |m| format_turn(m) }.join("\n\n")
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
# -- content polymorphism
|
|
86
|
+
def format_turn(msg)
|
|
86
87
|
role = (msg[:role] || msg['role'] || 'unknown').capitalize
|
|
87
88
|
content = msg[:content] || msg['content']
|
|
88
89
|
text = if content.is_a?(Array)
|
|
@@ -121,7 +122,8 @@ module RubynCode
|
|
|
121
122
|
)
|
|
122
123
|
end
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
# -- response parsing with multiple fallbacks
|
|
126
|
+
def parse_response(response)
|
|
125
127
|
return [] if response.nil?
|
|
126
128
|
|
|
127
129
|
text = if response.respond_to?(:content)
|
|
@@ -84,7 +84,8 @@ module RubynCode
|
|
|
84
84
|
# @param task_type [Symbol]
|
|
85
85
|
# @param client [LLM::Client, nil] active client (for provider checks)
|
|
86
86
|
# @return [Hash] { provider:, model: }
|
|
87
|
-
|
|
87
|
+
# -- multi-source fallback chain
|
|
88
|
+
def resolve(task_type, client: nil)
|
|
88
89
|
tier = tier_for(task_type)
|
|
89
90
|
active = active_provider
|
|
90
91
|
|
|
@@ -51,7 +51,7 @@ module RubynCode
|
|
|
51
51
|
klass
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
def create_tool_class(tool_name, description, parameters, mcp_client, remote_name) # rubocop:disable Metrics/
|
|
54
|
+
def create_tool_class(tool_name, description, parameters, mcp_client, remote_name) # rubocop:disable Metrics/MethodLength -- dynamic class creation requires setting many constants
|
|
55
55
|
bridge = self
|
|
56
56
|
|
|
57
57
|
Class.new(Tools::Base) do
|
|
@@ -80,7 +80,8 @@ module RubynCode
|
|
|
80
80
|
}
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
# -- assembles multi-field summary
|
|
84
|
+
def build_session_summary_lines(session_id, turns, totals)
|
|
84
85
|
avg_cost = turns.positive? ? totals[:cost] / turns : 0.0
|
|
85
86
|
[
|
|
86
87
|
header('Session Summary'),
|
|
@@ -104,7 +105,8 @@ module RubynCode
|
|
|
104
105
|
).to_a
|
|
105
106
|
end
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
# -- assembles multi-field daily summary
|
|
109
|
+
def build_daily_summary_lines(today, rows)
|
|
108
110
|
total_input = rows.sum { |r| fetch_int(r, 'input_tokens') }
|
|
109
111
|
total_output = rows.sum { |r| fetch_int(r, 'output_tokens') }
|
|
110
112
|
total_cost = rows.sum { |r| fetch_float(r, 'cost_usd') }
|