rubyn-code 0.4.0 → 0.5.1
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 +247 -9
- 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 +87 -13
- 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/megaplan.rb +50 -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 +3 -1
- data/lib/rubyn_code/cli/repl_setup.rb +38 -1
- data/lib/rubyn_code/cli/setup.rb +13 -0
- 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/plan_interview_answer_handler.rb +65 -0
- data/lib/rubyn_code/ide/handlers/plan_interview_cancel_handler.rb +22 -0
- data/lib/rubyn_code/ide/handlers/plan_interview_start_handler.rb +53 -0
- data/lib/rubyn_code/ide/handlers/plan_propose_handler.rb +41 -0
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +6 -3
- data/lib/rubyn_code/ide/handlers/recover_ci_handler.rb +132 -0
- data/lib/rubyn_code/ide/handlers/review_handler.rb +19 -2
- data/lib/rubyn_code/ide/handlers.rb +17 -2
- data/lib/rubyn_code/ide/protocol.rb +17 -1
- data/lib/rubyn_code/ide/server.rb +39 -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/megaplan/ci_recovery.rb +104 -0
- data/lib/rubyn_code/megaplan/interview_session.rb +245 -0
- data/lib/rubyn_code/megaplan/plan_proposer.rb +153 -0
- 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 +20 -0
- data/skills/megaplan/megaplan.md +156 -0
- data/skills/rubyn_self_test.md +75 -0
- metadata +25 -4
data/lib/rubyn_code/cli/app.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module RubynCode
|
|
4
4
|
module CLI
|
|
5
|
-
class App
|
|
5
|
+
class App # rubocop:disable Metrics/ClassLength -- CLI dispatch requires many small methods
|
|
6
6
|
def self.start(argv)
|
|
7
7
|
new(argv).run
|
|
8
8
|
end
|
|
@@ -26,6 +26,7 @@ module RubynCode
|
|
|
26
26
|
rubyn-code --resume [ID] Resume a previous session
|
|
27
27
|
rubyn-code --setup Pin rubyn-code to bypass rbenv/rvm
|
|
28
28
|
rubyn-code --auth Authenticate with Claude
|
|
29
|
+
rubyn-code --install-skills NAME Install a skill pack from the registry
|
|
29
30
|
rubyn-code --ide Start IDE server (VS Code extension)
|
|
30
31
|
rubyn-code --permission-mode MODE Set permission mode (default, accept_edits, plan_only, auto, dont_ask, bypass)
|
|
31
32
|
rubyn-code --version Show version
|
|
@@ -47,6 +48,9 @@ module RubynCode
|
|
|
47
48
|
/cost Show usage costs
|
|
48
49
|
/tasks List tasks
|
|
49
50
|
/skill [name] Load or list skills
|
|
51
|
+
/skills List installed and community skill packs
|
|
52
|
+
/install-skills Install skill packs from rubyn.ai
|
|
53
|
+
/remove-skills Remove an installed skill pack
|
|
50
54
|
|
|
51
55
|
Environment:
|
|
52
56
|
Config: ~/.rubyn-code/config.yml
|
|
@@ -60,23 +64,25 @@ module RubynCode
|
|
|
60
64
|
'--auth' => :auth, '--setup' => :setup
|
|
61
65
|
}.freeze
|
|
62
66
|
BOOLEAN_FLAGS = { '--yolo' => :yolo, '--debug' => :debug, '--skip-setup' => :skip_setup, '--ide' => :ide }.freeze
|
|
63
|
-
VALUE_FLAGS = { '--permission-mode' => :permission_mode }.freeze
|
|
67
|
+
VALUE_FLAGS = { '--permission-mode' => :permission_mode, '--dir' => :workspace_dir }.freeze
|
|
64
68
|
DAEMON_INT_FLAGS = { '--max-runs' => :max_runs, '--idle-timeout' => :idle_timeout,
|
|
65
69
|
'--poll-interval' => :poll_interval }.freeze
|
|
66
70
|
DAEMON_STR_FLAGS = { '--name' => :agent_name, '--role' => :role }.freeze
|
|
67
71
|
|
|
68
72
|
private
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
# -- unavoidable dispatch switch
|
|
75
|
+
def dispatch_command(command)
|
|
71
76
|
case command
|
|
72
|
-
when :version
|
|
73
|
-
when :auth
|
|
74
|
-
when :setup
|
|
75
|
-
when :help
|
|
76
|
-
when :run
|
|
77
|
-
when :ide
|
|
78
|
-
when :daemon
|
|
79
|
-
when :
|
|
77
|
+
when :version then puts "rubyn-code #{RubynCode::VERSION}"
|
|
78
|
+
when :auth then run_auth
|
|
79
|
+
when :setup then run_setup
|
|
80
|
+
when :help then display_help
|
|
81
|
+
when :run then run_single_prompt(@options[:prompt])
|
|
82
|
+
when :ide then run_ide
|
|
83
|
+
when :daemon then run_daemon
|
|
84
|
+
when :install_skills then run_install_skills(@options[:install_skills_names])
|
|
85
|
+
when :repl then run_repl
|
|
80
86
|
end
|
|
81
87
|
end
|
|
82
88
|
|
|
@@ -116,6 +122,10 @@ module RubynCode
|
|
|
116
122
|
options[:command] = :run
|
|
117
123
|
options[:prompt] = argv[idx + 1]
|
|
118
124
|
idx + 1
|
|
125
|
+
when '--install-skills'
|
|
126
|
+
options[:command] = :install_skills
|
|
127
|
+
options[:install_skills_names] = collect_pack_names(argv, idx + 1)
|
|
128
|
+
argv.length - 1
|
|
119
129
|
when 'daemon'
|
|
120
130
|
options[:command] = :daemon
|
|
121
131
|
parse_daemon_options!(argv, idx + 1, options)
|
|
@@ -155,7 +165,8 @@ module RubynCode
|
|
|
155
165
|
idx
|
|
156
166
|
end
|
|
157
167
|
|
|
158
|
-
|
|
168
|
+
# -- option dispatch with hash lookup
|
|
169
|
+
def parse_daemon_value_option(argv, idx, options)
|
|
159
170
|
arg = argv[idx]
|
|
160
171
|
daemon = options[:daemon]
|
|
161
172
|
if DAEMON_INT_FLAGS.key?(arg)
|
|
@@ -198,13 +209,76 @@ module RubynCode
|
|
|
198
209
|
|
|
199
210
|
def run_ide
|
|
200
211
|
mode = resolve_permission_mode
|
|
201
|
-
IDE::Server.new(permission_mode: mode).run
|
|
212
|
+
IDE::Server.new(permission_mode: mode, workspace_path: @options[:workspace_dir]).run
|
|
202
213
|
end
|
|
203
214
|
|
|
204
215
|
def run_daemon
|
|
205
216
|
DaemonRunner.new(@options).run
|
|
206
217
|
end
|
|
207
218
|
|
|
219
|
+
def run_install_skills(names)
|
|
220
|
+
renderer = Renderer.new
|
|
221
|
+
client = Skills::RegistryClient.new
|
|
222
|
+
installer = Skills::PackInstaller.new(
|
|
223
|
+
registry_client: client,
|
|
224
|
+
project_root: Dir.pwd,
|
|
225
|
+
global: @options[:install_skills_global]
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if names.empty? || names.include?('--update')
|
|
229
|
+
renderer.info('Checking for updates on all installed packs...')
|
|
230
|
+
results = installer.update_all do |event, data|
|
|
231
|
+
report_install_progress(renderer, event, data)
|
|
232
|
+
end
|
|
233
|
+
updated = results.select { |r| r[:status] == :installed }
|
|
234
|
+
if updated.empty?
|
|
235
|
+
renderer.info('All packs are up to date.')
|
|
236
|
+
else
|
|
237
|
+
renderer.info("Updated #{updated.size} pack(s).")
|
|
238
|
+
end
|
|
239
|
+
else
|
|
240
|
+
installer.install(names) do |event, data|
|
|
241
|
+
report_install_progress(renderer, event, data)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
rescue Skills::RegistryError => e
|
|
245
|
+
renderer.error("Registry error: #{e.message}")
|
|
246
|
+
exit(1)
|
|
247
|
+
rescue StandardError => e
|
|
248
|
+
renderer.error("Install failed: #{e.message}")
|
|
249
|
+
exit(1)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def report_install_progress(renderer, event, data)
|
|
253
|
+
case event
|
|
254
|
+
when :fetching then renderer.info("Fetching #{data[:name]} from rubyn.ai...")
|
|
255
|
+
when :downloading then renderer.info(" Downloading #{data[:total]} skill files...")
|
|
256
|
+
when :installed
|
|
257
|
+
data[:files].each { |f| puts " → #{data[:name]}/#{f}" }
|
|
258
|
+
renderer.info("Installed #{data[:files].size} skills to .rubyn-code/skills/#{data[:name]}/")
|
|
259
|
+
when :up_to_date then renderer.info("#{data[:name]} is already up to date (v#{data[:version]}).")
|
|
260
|
+
when :error then renderer.error("Failed: #{data[:name]}: #{data[:message]}")
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def collect_pack_names(argv, start)
|
|
265
|
+
names = []
|
|
266
|
+
idx = start
|
|
267
|
+
while idx < argv.length
|
|
268
|
+
arg = argv[idx]
|
|
269
|
+
break if arg.start_with?('-') && arg != '--update' && arg != '--global'
|
|
270
|
+
|
|
271
|
+
if arg == '--global'
|
|
272
|
+
@options ||= {}
|
|
273
|
+
@options[:install_skills_global] = true
|
|
274
|
+
else
|
|
275
|
+
names << arg
|
|
276
|
+
end
|
|
277
|
+
idx += 1
|
|
278
|
+
end
|
|
279
|
+
names
|
|
280
|
+
end
|
|
281
|
+
|
|
208
282
|
def run_repl
|
|
209
283
|
maybe_first_run!
|
|
210
284
|
REPL.new(
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class InstallSkills < Base
|
|
7
|
+
def self.command_name = '/install-skills'
|
|
8
|
+
def self.description = 'Install skill packs from the Rubyn registry'
|
|
9
|
+
|
|
10
|
+
def execute(args, ctx)
|
|
11
|
+
if args.empty?
|
|
12
|
+
ctx.renderer.warning('Usage: /install-skills <pack-name> [pack-name ...]')
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
pack_manager = RubynCode::Skills::PackManager.new
|
|
17
|
+
registry = RubynCode::Skills::RegistryClient.new
|
|
18
|
+
|
|
19
|
+
args.each { |name| install_pack(name, registry, pack_manager, ctx) }
|
|
20
|
+
rescue RubynCode::Skills::RegistryError => e
|
|
21
|
+
ctx.renderer.error(e.message)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def install_pack(name, registry, pack_manager, ctx)
|
|
27
|
+
if pack_manager.installed?(name)
|
|
28
|
+
ctx.renderer.warning("Pack '#{name}' is already installed. Use /remove-skills first to reinstall.")
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
ctx.renderer.info("Fetching pack '#{name}' from registry...")
|
|
33
|
+
result = registry.fetch_pack(name)
|
|
34
|
+
pack_manager.install(result[:data], etag: result[:etag])
|
|
35
|
+
ctx.renderer.info("Installed skill pack '#{name}' successfully.")
|
|
36
|
+
rescue RubynCode::Skills::RegistryError => e
|
|
37
|
+
ctx.renderer.error("Failed to install '#{name}': #{e.message}")
|
|
38
|
+
rescue ArgumentError => e
|
|
39
|
+
ctx.renderer.error("Invalid pack data for '#{name}': #{e.message}")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# `/megaplan` REPL command — mirrors the VS Code chat's chat-resident
|
|
7
|
+
# megaplan entry. Loads the megaplan skill (from the shared
|
|
8
|
+
# skills catalog) into the conversation, flips plan mode ON so the
|
|
9
|
+
# agent is restricted to read-only tools, and kicks off a
|
|
10
|
+
# conversational interview.
|
|
11
|
+
#
|
|
12
|
+
# The interview itself is chat-style here (multi-turn natural-
|
|
13
|
+
# language Q&A), not the structured question-card UI the VS Code
|
|
14
|
+
# extension uses. Same skill content driving both surfaces.
|
|
15
|
+
class Megaplan < Base
|
|
16
|
+
def self.command_name = '/megaplan'
|
|
17
|
+
def self.description = 'Plan a feature in phases (interview, then numbered phase docs)'
|
|
18
|
+
def self.aliases = ['/mega-plan'].freeze
|
|
19
|
+
|
|
20
|
+
def execute(args, ctx)
|
|
21
|
+
content = ctx.skill_loader.load('megaplan')
|
|
22
|
+
ctx.conversation.add_user_message("<skill>#{content}</skill>")
|
|
23
|
+
ctx.renderer.info('Megaplan mode — interviewer with read-only tools 🧠')
|
|
24
|
+
|
|
25
|
+
ctx.send_message(build_prompt(args.join(' ').strip))
|
|
26
|
+
|
|
27
|
+
{ action: :set_plan_mode, enabled: true }
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
ctx.renderer.error("Megaplan error: #{e.message}")
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_prompt(feature)
|
|
36
|
+
base = <<~PROMPT.strip
|
|
37
|
+
Conduct a megaplan interview, following the megaplan skill loaded above.
|
|
38
|
+
Stay strictly read-only: you may inspect files, search code, and check
|
|
39
|
+
git state, but do NOT edit, write, run shell mutations, or call any
|
|
40
|
+
destructive tool. Ask ONE question at a time. When you have enough,
|
|
41
|
+
output the final phase breakdown as a numbered outline.
|
|
42
|
+
PROMPT
|
|
43
|
+
return base if feature.empty?
|
|
44
|
+
|
|
45
|
+
"#{base}\n\nThe feature to plan: #{feature}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
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,9 @@ 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,
|
|
27
|
+
Commands::Megaplan
|
|
26
28
|
].each { |cmd| @command_registry.register(cmd) }
|
|
27
29
|
end
|
|
28
30
|
|
|
@@ -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
|
|