rubik 0.3.3 → 0.3.5
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/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +102 -0
- data/lib/rubik/adapter/process.rb +11 -8
- data/lib/rubik/ast/go_extractor.rb +9 -2
- data/lib/rubik/ast/rust_extractor.rb +9 -2
- data/lib/rubik/ast/typescript_extractor.rb +13 -7
- data/lib/rubik/buffer/manager.rb +99 -69
- data/lib/rubik/config/defaults.rb +17 -20
- data/lib/rubik/config.rb +0 -6
- data/lib/rubik/context/git_lister.rb +3 -1
- data/lib/rubik/llm/anthropic.rb +5 -2
- data/lib/rubik/llm/client.rb +102 -72
- data/lib/rubik/llm/openai_compatible.rb +83 -68
- data/lib/rubik/lsp/client.rb +320 -96
- data/lib/rubik/lsp/manager.rb +83 -12
- data/lib/rubik/lsp/server_command.rb +102 -0
- data/lib/rubik/orchestrator/goal_ledger/patch.rb +48 -30
- data/lib/rubik/orchestrator/lsp_feedback.rb +20 -4
- data/lib/rubik/orchestrator/subagent/reuse.rb +2 -2
- data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +60 -31
- data/lib/rubik/platform/process.rb +6 -6
- data/lib/rubik/platform/sanitize.rb +37 -16
- data/lib/rubik/prompt/doctrine.rb +4 -0
- data/lib/rubik/repl/commands/agent_context.rb +126 -104
- data/lib/rubik/repl/commands/agent_execution.rb +183 -155
- data/lib/rubik/repl/commands/agent_models.rb +112 -87
- data/lib/rubik/repl/commands/agent_providers.rb +190 -156
- data/lib/rubik/repl/commands/buffer.rb +122 -88
- data/lib/rubik/repl/commands/lsp.rb +212 -179
- data/lib/rubik/repl/commands/mcp.rb +120 -92
- data/lib/rubik/repl/commands/session.rb +90 -59
- data/lib/rubik/repl/commands/workspace.rb +266 -246
- data/lib/rubik/repl/shell.rb +10 -1
- data/lib/rubik/runtime/discovery.rb +12 -6
- data/lib/rubik/runtime/overlay_storage.rb +59 -2
- data/lib/rubik/sandbox/driver/docker.rb +2 -2
- data/lib/rubik/session/replayer.rb +1 -2
- data/lib/rubik/storage/store.rb +3 -2
- data/lib/rubik/storage/vakanuvis/distill_gate.rb +6 -2
- data/lib/rubik/storage/vakanuvis/gate.rb +37 -16
- data/lib/rubik/storage/vakanuvis/harness.rb +36 -21
- data/lib/rubik/storage/vakanuvis/memory_steward.rb +12 -6
- data/lib/rubik/tools/builtins/lifecycle.rb +34 -7
- data/lib/rubik/tools/builtins/lsp.rb +36 -8
- data/lib/rubik/tools/fuzzy_matcher.rb +27 -11
- data/lib/rubik/version.rb +1 -1
- metadata +2 -1
|
@@ -34,173 +34,207 @@ module Rubik
|
|
|
34
34
|
when "list", "ls", "status"
|
|
35
35
|
render_provider_catalog(ctx)
|
|
36
36
|
when "add"
|
|
37
|
-
|
|
38
|
-
arg2 = clean_args[2]
|
|
39
|
-
arg3 = clean_args[3]
|
|
40
|
-
|
|
41
|
-
p_type = "openai_compatible"
|
|
42
|
-
p_endpoint = nil
|
|
43
|
-
|
|
44
|
-
if arg2 && (arg2.start_with?("http://") || arg2.start_with?("https://"))
|
|
45
|
-
p_endpoint = arg2
|
|
46
|
-
elsif arg2 && arg3
|
|
47
|
-
p_type = arg2.downcase
|
|
48
|
-
p_endpoint = arg3
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
if p_name.nil? || p_name.empty? || p_endpoint.nil? || p_endpoint.empty?
|
|
52
|
-
ctx.out.puts "Usage: !provider add <name> [type] <endpoint_url> " \
|
|
53
|
-
"[--key=API_KEY | --key-env=ENV_VAR] [--model=NAME] [--context=LIMIT] [--project|--global]"
|
|
54
|
-
elsif !p_endpoint.start_with?("http://") && !p_endpoint.start_with?("https://")
|
|
55
|
-
ctx.out.puts "Error: Endpoint must be a valid HTTP/HTTPS URL (starting with http:// or https://). " \
|
|
56
|
-
"Received: '#{p_endpoint}'\nUsage: !provider add <name> [type] <endpoint_url> " \
|
|
57
|
-
"[--key=API_KEY | --key-env=ENV_VAR] [--model=NAME] [--context=LIMIT] [--project|--global]"
|
|
58
|
-
else
|
|
59
|
-
key_arg = clean_args.find { |a| a.start_with?("--key=") || a.start_with?("--api-key=") }
|
|
60
|
-
key_env_arg = clean_args.find { |a| a.start_with?("--key-env=") }
|
|
61
|
-
model_arg = clean_args.find { |a| a.start_with?("--model=") }
|
|
62
|
-
context_arg = clean_args.find { |a| a.start_with?("--context=") }
|
|
63
|
-
|
|
64
|
-
api_key = key_arg&.split("=", 2)&.last
|
|
65
|
-
key_env = key_env_arg&.split("=", 2)&.last
|
|
66
|
-
m_name = model_arg ? model_arg.split("=", 2).last : "default"
|
|
67
|
-
c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
|
|
68
|
-
|
|
69
|
-
normalized_endpoint = if ["openai_compatible", "openai"].include?(p_type)
|
|
70
|
-
Rubik::LLM::OpenAICompatibleProvider.normalize_endpoint(p_endpoint)
|
|
71
|
-
else
|
|
72
|
-
p_endpoint
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
p_spec = {
|
|
76
|
-
"type" => p_type,
|
|
77
|
-
"endpoint" => normalized_endpoint,
|
|
78
|
-
"api_key" => api_key,
|
|
79
|
-
"api_key_env" => key_env,
|
|
80
|
-
"models" => [
|
|
81
|
-
{ "name" => m_name, "context_limit" => c_limit }
|
|
82
|
-
]
|
|
83
|
-
}.compact
|
|
84
|
-
|
|
85
|
-
res = ctx.config.set("providers.#{p_name}", p_spec, persist: scope)
|
|
86
|
-
ctx.refresh_llm_registry!
|
|
87
|
-
next if Base.report_persist_failure(ctx, res, scope)
|
|
88
|
-
|
|
89
|
-
ctx.out.puts "Successfully added provider '#{p_name}' (#{p_type} -> #{normalized_endpoint}) " \
|
|
90
|
-
"with model '#{m_name}' [#{Base.scope_label(scope)}]."
|
|
91
|
-
end
|
|
37
|
+
handle_add_provider(clean_args, ctx, scope)
|
|
92
38
|
when "add-model", "model-add"
|
|
93
|
-
|
|
94
|
-
m_name = clean_args[2]
|
|
95
|
-
if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
|
|
96
|
-
ctx.out.puts "Usage: !provider add-model <provider_name> <model_name> [--context=LIMIT] [--project|--global]"
|
|
97
|
-
else
|
|
98
|
-
context_arg = clean_args.find { |a| a.start_with?("--context=") }
|
|
99
|
-
c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
|
|
100
|
-
|
|
101
|
-
res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: c_limit, persist: scope)
|
|
102
|
-
ctx.refresh_llm_registry!
|
|
103
|
-
if res[:ok]
|
|
104
|
-
ctx.out.puts "Added model '#{m_name}' (#{c_limit} tokens) to provider '#{p_name}' [#{Base.scope_label(scope)}]."
|
|
105
|
-
elsif res[:reason] == :unknown_provider
|
|
106
|
-
ctx.out.puts "Provider '#{p_name}' not found. Add it first via !provider add."
|
|
107
|
-
else
|
|
108
|
-
Base.report_persist_failure(ctx, res, scope)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
39
|
+
handle_add_provider_model(clean_args, ctx, scope)
|
|
111
40
|
when "remove-model", "model-remove", "model-rm"
|
|
112
|
-
|
|
113
|
-
m_name = clean_args[2]
|
|
114
|
-
if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
|
|
115
|
-
ctx.out.puts "Usage: !provider remove-model <provider_name> <model_name> [--project|--global]"
|
|
116
|
-
else
|
|
117
|
-
res = ctx.config.remove_provider_model(p_name, m_name, persist: scope)
|
|
118
|
-
ctx.refresh_llm_registry!
|
|
119
|
-
if res[:ok]
|
|
120
|
-
ctx.out.puts "Removed model '#{m_name}' from provider '#{p_name}' [#{Base.scope_label(scope)}]."
|
|
121
|
-
elsif res[:reason] == :unknown_provider
|
|
122
|
-
ctx.out.puts "Provider '#{p_name}' not found."
|
|
123
|
-
else
|
|
124
|
-
Base.report_persist_failure(ctx, res, scope)
|
|
125
|
-
end
|
|
126
|
-
end
|
|
41
|
+
handle_remove_provider_model(clean_args, ctx, scope)
|
|
127
42
|
when "set"
|
|
128
|
-
|
|
129
|
-
field = clean_args[2]&.downcase
|
|
130
|
-
val = clean_args[3]
|
|
131
|
-
if p_name.nil? || p_name.empty? || field.nil? || val.nil?
|
|
132
|
-
ctx.out.puts "Usage: !provider set <name> <endpoint|key|key-env|type|stream|stream_timeout|open_timeout|tool_native|num_ctx> <value> [--project|--global]"
|
|
133
|
-
else
|
|
134
|
-
p_spec = ctx.config.provider_spec(p_name)
|
|
135
|
-
if p_spec.nil?
|
|
136
|
-
ctx.out.puts "Provider '#{p_name}' not found."
|
|
137
|
-
else
|
|
138
|
-
case field
|
|
139
|
-
when "key", "api_key"
|
|
140
|
-
p_spec["api_key"] = val
|
|
141
|
-
when "key-env", "key_env", "api_key_env"
|
|
142
|
-
p_spec["api_key_env"] = val
|
|
143
|
-
when "endpoint", "url", "host"
|
|
144
|
-
if !val.start_with?("http://") && !val.start_with?("https://")
|
|
145
|
-
ctx.out.puts "Error: Endpoint must be a valid HTTP/HTTPS URL (starting with http:// or https://). Received: '#{val}'"
|
|
146
|
-
next
|
|
147
|
-
end
|
|
148
|
-
p_spec["endpoint"] = val
|
|
149
|
-
when "type"
|
|
150
|
-
p_spec["type"] = val
|
|
151
|
-
else
|
|
152
|
-
# Free-form fields (stream, tool_native, num_ctx, ...) are
|
|
153
|
-
# typed through parse_scalar_value so "false" becomes a
|
|
154
|
-
# real false — a raw "false" String is truthy in Ruby and
|
|
155
|
-
# would silently re-enable the flag it means to disable.
|
|
156
|
-
p_spec[field] = ctx.config.parse_scalar_value(val)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
res = ctx.config.set("providers.#{p_name}", p_spec, persist: scope)
|
|
160
|
-
ctx.refresh_llm_registry!
|
|
161
|
-
next if Base.report_persist_failure(ctx, res, scope)
|
|
162
|
-
|
|
163
|
-
ctx.out.puts "Updated provider '#{p_name}' field '#{field}' to '#{val}' [#{Base.scope_label(scope)}]."
|
|
164
|
-
end
|
|
165
|
-
end
|
|
43
|
+
handle_set_provider_field(clean_args, ctx, scope)
|
|
166
44
|
when "remove", "rm", "delete"
|
|
167
|
-
|
|
168
|
-
if p_name.nil? || p_name.empty?
|
|
169
|
-
ctx.out.puts "Usage: !provider remove <name> [--project|--global]"
|
|
170
|
-
else
|
|
171
|
-
res = ctx.config.remove_provider(p_name, persist: scope)
|
|
172
|
-
ctx.refresh_llm_registry!
|
|
173
|
-
if res[:ok]
|
|
174
|
-
ctx.out.puts "Removed provider '#{p_name}' [#{Base.scope_label(scope)}]."
|
|
175
|
-
elsif res[:reason] == :unknown_provider
|
|
176
|
-
ctx.out.puts "Provider '#{p_name}' not found."
|
|
177
|
-
else
|
|
178
|
-
Base.report_persist_failure(ctx, res, scope)
|
|
179
|
-
end
|
|
180
|
-
end
|
|
45
|
+
handle_remove_provider(clean_args, ctx, scope)
|
|
181
46
|
when "set-context", "context", "limit"
|
|
182
|
-
|
|
183
|
-
m_name = clean_args[2]
|
|
184
|
-
limit_val = clean_args[3]&.to_i
|
|
185
|
-
if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty? || limit_val.nil? || limit_val <= 0
|
|
186
|
-
ctx.out.puts "Usage: !provider set-context <provider> <model> <limit_in_tokens> [--project|--global]"
|
|
187
|
-
else
|
|
188
|
-
res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: limit_val, persist: scope)
|
|
189
|
-
ctx.refresh_llm_registry!
|
|
190
|
-
if res[:ok]
|
|
191
|
-
ctx.out.puts "Updated context limit for model '#{m_name}' on provider '#{p_name}' to #{limit_val} tokens [#{Base.scope_label(scope)}]."
|
|
192
|
-
elsif res[:reason] == :unknown_provider
|
|
193
|
-
ctx.out.puts "Provider '#{p_name}' not found."
|
|
194
|
-
else
|
|
195
|
-
Base.report_persist_failure(ctx, res, scope)
|
|
196
|
-
end
|
|
197
|
-
end
|
|
47
|
+
handle_set_provider_context(clean_args, ctx, scope)
|
|
198
48
|
else
|
|
199
49
|
ctx.out.puts USAGE_PROVIDER
|
|
200
50
|
end
|
|
201
51
|
end
|
|
202
52
|
end
|
|
203
53
|
|
|
54
|
+
private_class_method def self.handle_add_provider(clean_args, ctx, scope)
|
|
55
|
+
p_name = clean_args[1]&.downcase
|
|
56
|
+
arg2 = clean_args[2]
|
|
57
|
+
arg3 = clean_args[3]
|
|
58
|
+
|
|
59
|
+
p_type = "openai_compatible"
|
|
60
|
+
p_endpoint = nil
|
|
61
|
+
|
|
62
|
+
if arg2 && (arg2.start_with?("http://") || arg2.start_with?("https://"))
|
|
63
|
+
p_endpoint = arg2
|
|
64
|
+
elsif arg2 && arg3
|
|
65
|
+
p_type = arg2.downcase
|
|
66
|
+
p_endpoint = arg3
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if p_name.nil? || p_name.empty? || p_endpoint.nil? || p_endpoint.empty?
|
|
70
|
+
ctx.out.puts "Usage: !provider add <name> [type] <endpoint_url> " \
|
|
71
|
+
"[--key=API_KEY | --key-env=ENV_VAR] [--model=NAME] [--context=LIMIT] [--project|--global]"
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
unless p_endpoint.start_with?("http://") || p_endpoint.start_with?("https://")
|
|
76
|
+
ctx.out.puts "Error: Endpoint must be a valid HTTP/HTTPS URL (starting with http:// or https://). " \
|
|
77
|
+
"Received: '#{p_endpoint}'\nUsage: !provider add <name> [type] <endpoint_url> " \
|
|
78
|
+
"[--key=API_KEY | --key-env=ENV_VAR] [--model=NAME] [--context=LIMIT] [--project|--global]"
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
key_arg = clean_args.find { |a| a.start_with?("--key=") || a.start_with?("--api-key=") }
|
|
83
|
+
key_env_arg = clean_args.find { |a| a.start_with?("--key-env=") }
|
|
84
|
+
model_arg = clean_args.find { |a| a.start_with?("--model=") }
|
|
85
|
+
context_arg = clean_args.find { |a| a.start_with?("--context=") }
|
|
86
|
+
|
|
87
|
+
api_key = key_arg&.split("=", 2)&.last
|
|
88
|
+
key_env = key_env_arg&.split("=", 2)&.last
|
|
89
|
+
m_name = model_arg ? model_arg.split("=", 2).last : "default"
|
|
90
|
+
c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
|
|
91
|
+
|
|
92
|
+
normalized_endpoint = if ["openai_compatible", "openai"].include?(p_type)
|
|
93
|
+
Rubik::LLM::OpenAICompatibleProvider.normalize_endpoint(p_endpoint)
|
|
94
|
+
else
|
|
95
|
+
p_endpoint
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
p_spec = {
|
|
99
|
+
"type" => p_type,
|
|
100
|
+
"endpoint" => normalized_endpoint,
|
|
101
|
+
"api_key" => api_key,
|
|
102
|
+
"api_key_env" => key_env,
|
|
103
|
+
"models" => [
|
|
104
|
+
{ "name" => m_name, "context_limit" => c_limit }
|
|
105
|
+
]
|
|
106
|
+
}.compact
|
|
107
|
+
|
|
108
|
+
res = ctx.config.set("providers.#{p_name}", p_spec, persist: scope)
|
|
109
|
+
ctx.refresh_llm_registry!
|
|
110
|
+
return if Base.report_persist_failure(ctx, res, scope)
|
|
111
|
+
|
|
112
|
+
ctx.out.puts "Successfully added provider '#{p_name}' (#{p_type} -> #{normalized_endpoint}) " \
|
|
113
|
+
"with model '#{m_name}' [#{Base.scope_label(scope)}]."
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private_class_method def self.handle_add_provider_model(clean_args, ctx, scope)
|
|
117
|
+
p_name = clean_args[1]&.downcase
|
|
118
|
+
m_name = clean_args[2]
|
|
119
|
+
if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
|
|
120
|
+
ctx.out.puts "Usage: !provider add-model <provider_name> <model_name> [--context=LIMIT] [--project|--global]"
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context_arg = clean_args.find { |a| a.start_with?("--context=") }
|
|
125
|
+
c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
|
|
126
|
+
|
|
127
|
+
res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: c_limit, persist: scope)
|
|
128
|
+
ctx.refresh_llm_registry!
|
|
129
|
+
if res[:ok]
|
|
130
|
+
ctx.out.puts "Added model '#{m_name}' (#{c_limit} tokens) to provider '#{p_name}' [#{Base.scope_label(scope)}]."
|
|
131
|
+
elsif res[:reason] == :unknown_provider
|
|
132
|
+
ctx.out.puts "Provider '#{p_name}' not found. Add it first via !provider add."
|
|
133
|
+
else
|
|
134
|
+
Base.report_persist_failure(ctx, res, scope)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private_class_method def self.handle_remove_provider_model(clean_args, ctx, scope)
|
|
139
|
+
p_name = clean_args[1]&.downcase
|
|
140
|
+
m_name = clean_args[2]
|
|
141
|
+
if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
|
|
142
|
+
ctx.out.puts "Usage: !provider remove-model <provider_name> <model_name> [--project|--global]"
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
res = ctx.config.remove_provider_model(p_name, m_name, persist: scope)
|
|
147
|
+
ctx.refresh_llm_registry!
|
|
148
|
+
if res[:ok]
|
|
149
|
+
ctx.out.puts "Removed model '#{m_name}' from provider '#{p_name}' [#{Base.scope_label(scope)}]."
|
|
150
|
+
elsif res[:reason] == :unknown_provider
|
|
151
|
+
ctx.out.puts "Provider '#{p_name}' not found."
|
|
152
|
+
else
|
|
153
|
+
Base.report_persist_failure(ctx, res, scope)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private_class_method def self.handle_set_provider_field(clean_args, ctx, scope)
|
|
158
|
+
p_name = clean_args[1]&.downcase
|
|
159
|
+
field = clean_args[2]&.downcase
|
|
160
|
+
val = clean_args[3]
|
|
161
|
+
if p_name.nil? || p_name.empty? || field.nil? || val.nil?
|
|
162
|
+
ctx.out.puts "Usage: !provider set <name> <endpoint|key|key-env|type|stream|stream_timeout|open_timeout|tool_native|num_ctx> <value> [--project|--global]"
|
|
163
|
+
return
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
p_spec = ctx.config.provider_spec(p_name)
|
|
167
|
+
if p_spec.nil?
|
|
168
|
+
ctx.out.puts "Provider '#{p_name}' not found."
|
|
169
|
+
return
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
case field
|
|
173
|
+
when "key", "api_key"
|
|
174
|
+
p_spec["api_key"] = val
|
|
175
|
+
when "key-env", "key_env", "api_key_env"
|
|
176
|
+
p_spec["api_key_env"] = val
|
|
177
|
+
when "endpoint", "url", "host"
|
|
178
|
+
unless val.start_with?("http://") || val.start_with?("https://")
|
|
179
|
+
ctx.out.puts "Error: Endpoint must be a valid HTTP/HTTPS URL (starting with http:// or https://). Received: '#{val}'"
|
|
180
|
+
return
|
|
181
|
+
end
|
|
182
|
+
p_spec["endpoint"] = val
|
|
183
|
+
when "type"
|
|
184
|
+
p_spec["type"] = val
|
|
185
|
+
else
|
|
186
|
+
# Free-form fields (stream, tool_native, num_ctx, ...) are
|
|
187
|
+
# typed through parse_scalar_value so "false" becomes a
|
|
188
|
+
# real false — a raw "false" String is truthy in Ruby and
|
|
189
|
+
# would silently re-enable the flag it means to disable.
|
|
190
|
+
p_spec[field] = ctx.config.parse_scalar_value(val)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
res = ctx.config.set("providers.#{p_name}", p_spec, persist: scope)
|
|
194
|
+
ctx.refresh_llm_registry!
|
|
195
|
+
return if Base.report_persist_failure(ctx, res, scope)
|
|
196
|
+
|
|
197
|
+
ctx.out.puts "Updated provider '#{p_name}' field '#{field}' to '#{val}' [#{Base.scope_label(scope)}]."
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private_class_method def self.handle_remove_provider(clean_args, ctx, scope)
|
|
201
|
+
p_name = clean_args[1]&.downcase
|
|
202
|
+
if p_name.nil? || p_name.empty?
|
|
203
|
+
ctx.out.puts "Usage: !provider remove <name> [--project|--global]"
|
|
204
|
+
return
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
res = ctx.config.remove_provider(p_name, persist: scope)
|
|
208
|
+
ctx.refresh_llm_registry!
|
|
209
|
+
if res[:ok]
|
|
210
|
+
ctx.out.puts "Removed provider '#{p_name}' [#{Base.scope_label(scope)}]."
|
|
211
|
+
elsif res[:reason] == :unknown_provider
|
|
212
|
+
ctx.out.puts "Provider '#{p_name}' not found."
|
|
213
|
+
else
|
|
214
|
+
Base.report_persist_failure(ctx, res, scope)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private_class_method def self.handle_set_provider_context(clean_args, ctx, scope)
|
|
219
|
+
p_name = clean_args[1]&.downcase
|
|
220
|
+
m_name = clean_args[2]
|
|
221
|
+
limit_val = clean_args[3]&.to_i
|
|
222
|
+
if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty? || limit_val.nil? || limit_val <= 0
|
|
223
|
+
ctx.out.puts "Usage: !provider set-context <provider> <model> <limit_in_tokens> [--project|--global]"
|
|
224
|
+
return
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: limit_val, persist: scope)
|
|
228
|
+
ctx.refresh_llm_registry!
|
|
229
|
+
if res[:ok]
|
|
230
|
+
ctx.out.puts "Updated context limit for model '#{m_name}' on provider '#{p_name}' to #{limit_val} tokens [#{Base.scope_label(scope)}]."
|
|
231
|
+
elsif res[:reason] == :unknown_provider
|
|
232
|
+
ctx.out.puts "Provider '#{p_name}' not found."
|
|
233
|
+
else
|
|
234
|
+
Base.report_persist_failure(ctx, res, scope)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
204
238
|
private_class_method def self.register_providers_command(shell)
|
|
205
239
|
shell.register_command("providers", description: "List all configured LLM providers, endpoints, and models", audience: :both) do |_args, ctx|
|
|
206
240
|
render_provider_catalog(ctx)
|
|
@@ -16,6 +16,14 @@ module Rubik
|
|
|
16
16
|
USAGE_AUTOAPPLY = "Usage: !autoapply <manual | on_test | on_finish | always | status> [--project|--global]"
|
|
17
17
|
|
|
18
18
|
def self.register(shell)
|
|
19
|
+
register_diff_command(shell)
|
|
20
|
+
register_apply_command(shell)
|
|
21
|
+
register_undo_command(shell)
|
|
22
|
+
register_rollback_command(shell)
|
|
23
|
+
register_autoapply_command(shell)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private_class_method def self.register_diff_command(shell)
|
|
19
27
|
shell.register_command(
|
|
20
28
|
"diff",
|
|
21
29
|
description: "Preview unapplied buffer changes (staged edits that are NOT yet on disk). " \
|
|
@@ -23,111 +31,137 @@ module Rubik
|
|
|
23
31
|
"For ground-truth file content, use read_file; for git history, use run_command with git. " \
|
|
24
32
|
"(colorized viewer on terminals)",
|
|
25
33
|
audience: :both
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
target_path = args.first
|
|
29
|
-
if target_path && !target_path.empty?
|
|
30
|
-
rel = begin
|
|
31
|
-
ctx.buffer_manager.sanitize_path(target_path)
|
|
32
|
-
rescue
|
|
33
|
-
target_path
|
|
34
|
-
end
|
|
35
|
-
if ctx.buffer_manager.dirty_files.key?(rel)
|
|
36
|
-
full_path = File.expand_path(rel, ctx.buffer_manager.root_dir)
|
|
37
|
-
orig_content = File.exist?(full_path) ? File.read(full_path) : ""
|
|
38
|
-
new_content = ctx.buffer_manager.read(rel) || ""
|
|
39
|
-
Rubik::UI::DiffViewer.render(ctx, theme, Rubik::Buffer::Diff.unified_diff(rel, orig_content, new_content))
|
|
40
|
-
else
|
|
41
|
-
ctx.out.puts "No unapplied buffer changes for '#{target_path}'."
|
|
42
|
-
end
|
|
43
|
-
else
|
|
44
|
-
diff_text = ctx.buffer_manager.diff_all
|
|
45
|
-
if diff_text.empty?
|
|
46
|
-
ctx.out.puts "No unapplied buffer changes."
|
|
47
|
-
else
|
|
48
|
-
Rubik::UI::DiffViewer.render(ctx, theme, diff_text)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
34
|
+
) { |args, ctx| handle_diff(args, ctx) }
|
|
35
|
+
end
|
|
52
36
|
|
|
37
|
+
private_class_method def self.register_apply_command(shell)
|
|
53
38
|
shell.register_command("apply", description: "Apply dirty buffer changes to disk") do |args, ctx|
|
|
54
|
-
|
|
55
|
-
begin
|
|
56
|
-
if target_path && !target_path.empty?
|
|
57
|
-
applied = ctx.buffer_manager.apply!(target_path)
|
|
58
|
-
conflicts = ctx.buffer_manager.last_conflicts || []
|
|
59
|
-
if applied.empty? && conflicts.empty?
|
|
60
|
-
ctx.out.puts "No unapplied buffer changes for '#{target_path}'."
|
|
61
|
-
else
|
|
62
|
-
ctx.sync_context_and_ast(applied) unless applied.empty?
|
|
63
|
-
ctx.out.puts "Applied changes to file: #{applied.first}" unless applied.empty?
|
|
64
|
-
end
|
|
65
|
-
else
|
|
66
|
-
applied = ctx.buffer_manager.apply!
|
|
67
|
-
conflicts = ctx.buffer_manager.last_conflicts || []
|
|
68
|
-
if applied.empty? && conflicts.empty?
|
|
69
|
-
ctx.out.puts "No unapplied buffer changes."
|
|
70
|
-
else
|
|
71
|
-
ctx.sync_context_and_ast(applied) unless applied.empty?
|
|
72
|
-
ctx.out.puts "Applied changes to #{applied.size} file(s): #{applied.join(', ')}" unless applied.empty?
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
conflicts.each do |c|
|
|
76
|
-
ctx.out.puts "[CONFLICT] #{c}: disk content changed since last read — skipped to prevent data loss. Re-read the file to resolve."
|
|
77
|
-
end
|
|
78
|
-
rescue Rubik::Buffer::ApplyFailedError => e
|
|
79
|
-
# Partial apply (issue #146): the report names what DID reach
|
|
80
|
-
# disk before the failure — a mixed outcome must not be
|
|
81
|
-
# presented as a plain success or a plain failure.
|
|
82
|
-
lines = Rubik::Buffer::ApplyContract.failure_lines(e, sync: ->(paths) { ctx.sync_context_and_ast(paths) })
|
|
83
|
-
ctx.out.puts "❌ #{lines.first}"
|
|
84
|
-
lines[1..].each { |line| ctx.out.puts line }
|
|
85
|
-
end
|
|
39
|
+
handle_apply(args, ctx)
|
|
86
40
|
end
|
|
41
|
+
end
|
|
87
42
|
|
|
43
|
+
private_class_method def self.register_undo_command(shell)
|
|
88
44
|
shell.register_command("undo", description: "Revert the last snapshot", audience: :both) do |_args, ctx|
|
|
89
|
-
|
|
90
|
-
ctx.out.puts "Undo successful."
|
|
91
|
-
else
|
|
92
|
-
ctx.out.puts "No snapshots to undo."
|
|
93
|
-
end
|
|
45
|
+
handle_undo(ctx)
|
|
94
46
|
end
|
|
47
|
+
end
|
|
95
48
|
|
|
49
|
+
private_class_method def self.register_rollback_command(shell)
|
|
96
50
|
shell.register_command("rollback", description: "Rollback to a specific snapshot ID", usage: USAGE_ROLLBACK, audience: :both) do |args, ctx|
|
|
97
|
-
|
|
98
|
-
if sid.nil? || sid.empty?
|
|
99
|
-
ctx.out.puts USAGE_ROLLBACK
|
|
100
|
-
elsif ctx.snapshot_store.rollback(sid, ctx.buffer_manager)
|
|
101
|
-
ctx.out.puts "Rollback to snapshot #{sid} successful."
|
|
102
|
-
else
|
|
103
|
-
ctx.out.puts "Snapshot #{sid} not found."
|
|
104
|
-
end
|
|
51
|
+
handle_rollback(args, ctx)
|
|
105
52
|
end
|
|
53
|
+
end
|
|
106
54
|
|
|
55
|
+
private_class_method def self.register_autoapply_command(shell)
|
|
107
56
|
shell.register_command("autoapply",
|
|
108
57
|
description: "Get or set buffer auto-apply policy (manual|on_test|on_finish|always)",
|
|
109
58
|
usage: USAGE_AUTOAPPLY, audience: :both) do |args, ctx|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
ctx.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
59
|
+
handle_autoapply(args, ctx)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private_class_method def self.handle_diff(args, ctx)
|
|
64
|
+
theme = ctx.theme
|
|
65
|
+
target_path = args.first
|
|
66
|
+
if target_path && !target_path.empty?
|
|
67
|
+
rel = begin
|
|
68
|
+
ctx.buffer_manager.sanitize_path(target_path)
|
|
69
|
+
rescue
|
|
70
|
+
target_path
|
|
71
|
+
end
|
|
72
|
+
if ctx.buffer_manager.dirty_files.key?(rel)
|
|
73
|
+
full_path = File.expand_path(rel, ctx.buffer_manager.root_dir)
|
|
74
|
+
orig_content = File.exist?(full_path) ? File.read(full_path) : ""
|
|
75
|
+
new_content = ctx.buffer_manager.read(rel) || ""
|
|
76
|
+
Rubik::UI::DiffViewer.render(ctx, theme, Rubik::Buffer::Diff.unified_diff(rel, orig_content, new_content))
|
|
77
|
+
else
|
|
78
|
+
ctx.out.puts "No unapplied buffer changes for '#{target_path}'."
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
diff_text = ctx.buffer_manager.diff_all
|
|
82
|
+
if diff_text.empty?
|
|
83
|
+
ctx.out.puts "No unapplied buffer changes."
|
|
126
84
|
else
|
|
127
|
-
ctx
|
|
85
|
+
Rubik::UI::DiffViewer.render(ctx, theme, diff_text)
|
|
128
86
|
end
|
|
129
87
|
end
|
|
130
88
|
end
|
|
89
|
+
|
|
90
|
+
private_class_method def self.handle_apply(args, ctx)
|
|
91
|
+
target_path = args.first
|
|
92
|
+
begin
|
|
93
|
+
if target_path && !target_path.empty?
|
|
94
|
+
applied = ctx.buffer_manager.apply!(target_path)
|
|
95
|
+
conflicts = ctx.buffer_manager.last_conflicts || []
|
|
96
|
+
if applied.empty? && conflicts.empty?
|
|
97
|
+
ctx.out.puts "No unapplied buffer changes for '#{target_path}'."
|
|
98
|
+
else
|
|
99
|
+
ctx.sync_context_and_ast(applied) unless applied.empty?
|
|
100
|
+
ctx.out.puts "Applied changes to file: #{applied.first}" unless applied.empty?
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
applied = ctx.buffer_manager.apply!
|
|
104
|
+
conflicts = ctx.buffer_manager.last_conflicts || []
|
|
105
|
+
if applied.empty? && conflicts.empty?
|
|
106
|
+
ctx.out.puts "No unapplied buffer changes."
|
|
107
|
+
else
|
|
108
|
+
ctx.sync_context_and_ast(applied) unless applied.empty?
|
|
109
|
+
ctx.out.puts "Applied changes to #{applied.size} file(s): #{applied.join(', ')}" unless applied.empty?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
conflicts.each do |c|
|
|
113
|
+
ctx.out.puts "[CONFLICT] #{c}: disk content changed since last read — skipped to prevent data loss. Re-read the file to resolve."
|
|
114
|
+
end
|
|
115
|
+
rescue Rubik::Buffer::ApplyFailedError => e
|
|
116
|
+
# Partial apply (issue #146): the report names what DID reach
|
|
117
|
+
# disk before the failure — a mixed outcome must not be
|
|
118
|
+
# presented as a plain success or a plain failure.
|
|
119
|
+
lines = Rubik::Buffer::ApplyContract.failure_lines(e, sync: ->(paths) { ctx.sync_context_and_ast(paths) })
|
|
120
|
+
ctx.out.puts "❌ #{lines.first}"
|
|
121
|
+
lines[1..].each { |line| ctx.out.puts line }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private_class_method def self.handle_undo(ctx)
|
|
126
|
+
if ctx.snapshot_store.undo(ctx.buffer_manager)
|
|
127
|
+
ctx.out.puts "Undo successful."
|
|
128
|
+
else
|
|
129
|
+
ctx.out.puts "No snapshots to undo."
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private_class_method def self.handle_rollback(args, ctx)
|
|
134
|
+
sid = args.first
|
|
135
|
+
if sid.nil? || sid.empty?
|
|
136
|
+
ctx.out.puts USAGE_ROLLBACK
|
|
137
|
+
elsif ctx.snapshot_store.rollback(sid, ctx.buffer_manager)
|
|
138
|
+
ctx.out.puts "Rollback to snapshot #{sid} successful."
|
|
139
|
+
else
|
|
140
|
+
ctx.out.puts "Snapshot #{sid} not found."
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private_class_method def self.handle_autoapply(args, ctx)
|
|
145
|
+
scope, clean_args = Base.parse_scope(args)
|
|
146
|
+
sub = clean_args.first&.downcase
|
|
147
|
+
|
|
148
|
+
if sub.nil? || sub == "status"
|
|
149
|
+
cur = ctx.config.fetch("buffer.auto_apply", "manual")
|
|
150
|
+
can_m = ctx.config.fetch("buffer.model_can_apply", true)
|
|
151
|
+
ctx.out.puts "Buffer Auto-Apply Policy:"
|
|
152
|
+
ctx.out.puts " • Mode : #{cur} (manual | on_test | on_finish | always)"
|
|
153
|
+
ctx.out.puts " • Model Can Apply : #{can_m}"
|
|
154
|
+
ctx.out.puts " • Prompt Indicator : #{ctx.config.fetch('buffer.prompt_indicator', true)}"
|
|
155
|
+
ctx.out.puts " • Reminder Interval: #{ctx.config.fetch('buffer.reminder_interval', 3)} turns"
|
|
156
|
+
elsif ["manual", "on_test", "on_finish", "always"].include?(sub)
|
|
157
|
+
res = ctx.config.set("buffer.auto_apply", sub, persist: scope)
|
|
158
|
+
return if Base.report_persist_failure(ctx, res, scope)
|
|
159
|
+
|
|
160
|
+
ctx.out.puts "Buffer auto-apply policy set to '#{sub}' [#{Base.scope_label(scope)}]."
|
|
161
|
+
else
|
|
162
|
+
ctx.out.puts USAGE_AUTOAPPLY
|
|
163
|
+
end
|
|
164
|
+
end
|
|
131
165
|
end
|
|
132
166
|
end
|
|
133
167
|
end
|