openclacky 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/commit/SKILL.md +29 -4
  3. data/.clackyrules +3 -1
  4. data/CHANGELOG.md +103 -2
  5. data/README.md +70 -161
  6. data/bin/clarky +11 -0
  7. data/docs/HOW-TO-USE-CN.md +96 -0
  8. data/docs/HOW-TO-USE.md +94 -0
  9. data/docs/config.example.yml +27 -0
  10. data/docs/deploy_subagent_design.md +540 -0
  11. data/docs/time_machine_design.md +247 -0
  12. data/docs/why-openclacky.md +0 -1
  13. data/lib/clacky/agent/cost_tracker.rb +180 -0
  14. data/lib/clacky/agent/llm_caller.rb +54 -0
  15. data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
  16. data/lib/clacky/agent/message_compressor_helper.rb +534 -0
  17. data/lib/clacky/agent/session_serializer.rb +152 -0
  18. data/lib/clacky/agent/skill_manager.rb +138 -0
  19. data/lib/clacky/agent/system_prompt_builder.rb +96 -0
  20. data/lib/clacky/agent/time_machine.rb +199 -0
  21. data/lib/clacky/agent/tool_executor.rb +434 -0
  22. data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
  23. data/lib/clacky/agent.rb +260 -1370
  24. data/lib/clacky/agent_config.rb +447 -10
  25. data/lib/clacky/cli.rb +275 -98
  26. data/lib/clacky/client.rb +12 -2
  27. data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
  28. data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
  29. data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
  30. data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
  31. data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
  32. data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
  33. data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
  34. data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
  35. data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
  36. data/lib/clacky/default_skills/new/SKILL.md +2 -2
  37. data/lib/clacky/json_ui_controller.rb +195 -0
  38. data/lib/clacky/providers.rb +107 -0
  39. data/lib/clacky/skill.rb +48 -7
  40. data/lib/clacky/skill_loader.rb +7 -0
  41. data/lib/clacky/tools/edit.rb +105 -48
  42. data/lib/clacky/tools/file_reader.rb +44 -73
  43. data/lib/clacky/tools/invoke_skill.rb +89 -0
  44. data/lib/clacky/tools/list_tasks.rb +54 -0
  45. data/lib/clacky/tools/redo_task.rb +41 -0
  46. data/lib/clacky/tools/safe_shell.rb +1 -1
  47. data/lib/clacky/tools/shell.rb +74 -62
  48. data/lib/clacky/tools/trash_manager.rb +1 -1
  49. data/lib/clacky/tools/undo_task.rb +32 -0
  50. data/lib/clacky/tools/web_fetch.rb +2 -1
  51. data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
  52. data/lib/clacky/ui2/components/inline_input.rb +23 -2
  53. data/lib/clacky/ui2/components/input_area.rb +65 -21
  54. data/lib/clacky/ui2/components/modal_component.rb +199 -62
  55. data/lib/clacky/ui2/layout_manager.rb +75 -25
  56. data/lib/clacky/ui2/line_editor.rb +23 -2
  57. data/lib/clacky/ui2/markdown_renderer.rb +31 -10
  58. data/lib/clacky/ui2/screen_buffer.rb +2 -0
  59. data/lib/clacky/ui2/ui_controller.rb +316 -37
  60. data/lib/clacky/ui2.rb +2 -0
  61. data/lib/clacky/ui_interface.rb +50 -0
  62. data/lib/clacky/utils/arguments_parser.rb +31 -3
  63. data/lib/clacky/utils/file_processor.rb +13 -18
  64. data/lib/clacky/version.rb +1 -1
  65. data/lib/clacky.rb +19 -9
  66. data/scripts/install.sh +274 -97
  67. data/scripts/uninstall.sh +12 -12
  68. metadata +40 -13
  69. data/.clacky/skills/test-skill/SKILL.md +0 -15
  70. data/lib/clacky/compression/base.rb +0 -231
  71. data/lib/clacky/compression/standard.rb +0 -339
  72. data/lib/clacky/config.rb +0 -117
  73. /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
  74. /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
  75. /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
  76. /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
  77. /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
  78. /data/lib/clacky/{trash_directory.rb → utils/trash_directory.rb} +0 -0
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ module DeployTools
5
+ # Fetch runtime logs from deployed service
6
+ class FetchRuntimeLogs
7
+ DEFAULT_LINES = 100
8
+ MAX_LINES = 1000
9
+
10
+ # Fetch runtime logs
11
+ #
12
+ # @param service_name [String] Service to fetch logs from
13
+ # @param lines [Integer] Number of lines to fetch (default: 100)
14
+ # @return [Hash] Result containing logs
15
+ def self.execute(service_name:, lines: DEFAULT_LINES)
16
+ if service_name.nil? || service_name.empty?
17
+ return {
18
+ error: "Service name is required",
19
+ details: "Please provide a valid service name"
20
+ }
21
+ end
22
+
23
+ # Validate lines parameter
24
+ lines = lines.to_i
25
+ if lines <= 0 || lines > MAX_LINES
26
+ return {
27
+ error: "Invalid lines parameter",
28
+ details: "Lines must be between 1 and #{MAX_LINES}",
29
+ provided: lines
30
+ }
31
+ end
32
+
33
+ puts "📋 Fetching #{lines} lines of logs for service: #{service_name}"
34
+
35
+ # Execute command
36
+ command = "clackycli logs -s #{shell_escape(service_name)} --lines #{lines}"
37
+ output = `#{command} 2>&1`
38
+ exit_code = $?.exitstatus
39
+
40
+ if exit_code != 0
41
+ return {
42
+ error: "Failed to fetch logs",
43
+ details: output,
44
+ exit_code: exit_code,
45
+ service: service_name
46
+ }
47
+ end
48
+
49
+ {
50
+ success: true,
51
+ service: service_name,
52
+ lines_requested: lines,
53
+ logs: output,
54
+ timestamp: Time.now.iso8601
55
+ }
56
+ end
57
+
58
+ # Escape shell arguments
59
+ #
60
+ # @param str [String] String to escape
61
+ # @return [String] Escaped string
62
+ def self.shell_escape(str)
63
+ "'#{str.gsub("'", "'\\\\''")}'"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ module DeployTools
5
+ # List Railway services with environment variables (sensitive data masked)
6
+ class ListServices
7
+ SENSITIVE_PATTERNS = [
8
+ /password/i,
9
+ /secret/i,
10
+ /api_key/i,
11
+ /token/i,
12
+ /credential/i,
13
+ /private_key/i
14
+ ].freeze
15
+
16
+ # Execute the list_services command
17
+ #
18
+ # @return [Hash] Result containing services array
19
+ def self.execute
20
+ output = `clackycli service list --json 2>&1`
21
+ exit_code = $?.exitstatus
22
+
23
+ if exit_code != 0
24
+ return {
25
+ error: "Failed to list services",
26
+ details: output,
27
+ exit_code: exit_code
28
+ }
29
+ end
30
+
31
+ begin
32
+ services = JSON.parse(output)
33
+ masked_services = mask_sensitive_data(services)
34
+
35
+ {
36
+ success: true,
37
+ services: masked_services,
38
+ count: masked_services.length
39
+ }
40
+ rescue JSON::ParserError => e
41
+ {
42
+ error: "Failed to parse Railway CLI output",
43
+ details: e.message,
44
+ raw_output: output
45
+ }
46
+ end
47
+ end
48
+
49
+ # Mask sensitive environment variable values
50
+ #
51
+ # @param services [Array<Hash>] Array of service objects
52
+ # @return [Array<Hash>] Services with masked sensitive data
53
+ def self.mask_sensitive_data(services)
54
+ services.map do |service|
55
+ service = service.dup
56
+
57
+ if service['variables']
58
+ service['variables'] = mask_variables(service['variables'])
59
+ end
60
+
61
+ service
62
+ end
63
+ end
64
+
65
+ # Mask sensitive variable values
66
+ #
67
+ # @param variables [Hash] Environment variables
68
+ # @return [Hash] Variables with sensitive values masked
69
+ def self.mask_variables(variables)
70
+ variables.transform_values do |value|
71
+ next value unless value.is_a?(String)
72
+
73
+ # Check if variable name matches sensitive patterns
74
+ is_sensitive = SENSITIVE_PATTERNS.any? { |pattern| value =~ pattern }
75
+ is_sensitive ? '******' : value
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ module DeployTools
5
+ # Report deployment status to user with formatted output
6
+ class ReportDeployStatus
7
+ VALID_STATUSES = %w[analyzing deploying checking success failed].freeze
8
+
9
+ STATUS_ICONS = {
10
+ 'analyzing' => '🔍',
11
+ 'deploying' => '🚀',
12
+ 'checking' => '✅',
13
+ 'success' => '🎉',
14
+ 'failed' => '❌'
15
+ }.freeze
16
+
17
+ STATUS_COLORS = {
18
+ 'analyzing' => :cyan,
19
+ 'deploying' => :yellow,
20
+ 'checking' => :blue,
21
+ 'success' => :green,
22
+ 'failed' => :red
23
+ }.freeze
24
+
25
+ # Execute the report_deploy_status command
26
+ #
27
+ # @param status [String] Deployment status (analyzing, deploying, checking, success, failed)
28
+ # @param message [String] Status message to display
29
+ # @return [Hash] Result of the report operation
30
+ def self.execute(status:, message:)
31
+ unless VALID_STATUSES.include?(status)
32
+ return {
33
+ error: "Invalid status",
34
+ details: "Status must be one of: #{VALID_STATUSES.join(', ')}",
35
+ provided: status
36
+ }
37
+ end
38
+
39
+ icon = STATUS_ICONS[status]
40
+ formatted_message = format_message(status, message, icon)
41
+
42
+ # Output to stdout
43
+ puts formatted_message
44
+
45
+ {
46
+ success: true,
47
+ status: status,
48
+ message: message,
49
+ timestamp: Time.now.iso8601
50
+ }
51
+ end
52
+
53
+ # Format the status message with icon and styling
54
+ #
55
+ # @param status [String] Deployment status
56
+ # @param message [String] Status message
57
+ # @param icon [String] Emoji icon for status
58
+ # @return [String] Formatted message
59
+ def self.format_message(status, message, icon)
60
+ timestamp = Time.now.strftime("%H:%M:%S")
61
+ status_label = status.upcase.ljust(10)
62
+
63
+ "#{icon} [#{timestamp}] #{status_label} #{message}"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ module DeployTools
5
+ # Set environment variables for a Railway service
6
+ class SetDeployVariables
7
+ PROTECTED_PREFIXES = ['CLACKY_'].freeze
8
+
9
+ SENSITIVE_PATTERNS = [
10
+ /password/i,
11
+ /secret/i,
12
+ /api_key/i,
13
+ /token/i,
14
+ /credential/i,
15
+ /private_key/i
16
+ ].freeze
17
+
18
+ # Execute the set_deploy_variables command
19
+ #
20
+ # @param service_name [String] Target service name
21
+ # @param variables [Hash] Simple variables (KEY => VALUE)
22
+ # @param ref_variables [Hash] Reference variables (KEY => SERVICE.VAR)
23
+ # @return [Hash] Result of the operation
24
+ def self.execute(service_name:, variables: {}, ref_variables: {})
25
+ # Validate service name
26
+ if service_name.nil? || service_name.empty?
27
+ return {
28
+ error: "Service name is required",
29
+ details: "Please provide a valid service name"
30
+ }
31
+ end
32
+
33
+ results = {
34
+ success: true,
35
+ service: service_name,
36
+ set_variables: [],
37
+ skipped_variables: [],
38
+ errors: []
39
+ }
40
+
41
+ # Set simple variables
42
+ variables.each do |key, value|
43
+ result = set_variable(service_name, key, value, is_reference: false)
44
+ if result[:success]
45
+ results[:set_variables] << { key: key, type: 'simple' }
46
+ elsif result[:skipped]
47
+ results[:skipped_variables] << { key: key, reason: result[:reason] }
48
+ else
49
+ results[:errors] << { key: key, error: result[:error] }
50
+ end
51
+ end
52
+
53
+ # Set reference variables
54
+ ref_variables.each do |key, reference|
55
+ result = set_variable(service_name, key, reference, is_reference: true)
56
+ if result[:success]
57
+ results[:set_variables] << { key: key, type: 'reference' }
58
+ elsif result[:skipped]
59
+ results[:skipped_variables] << { key: key, reason: result[:reason] }
60
+ else
61
+ results[:errors] << { key: key, error: result[:error] }
62
+ end
63
+ end
64
+
65
+ # Overall success if no errors occurred
66
+ results[:success] = results[:errors].empty?
67
+ results
68
+ end
69
+
70
+ # Set a single environment variable
71
+ #
72
+ # @param service_name [String] Service name
73
+ # @param key [String] Variable name
74
+ # @param value [String] Variable value or reference
75
+ # @param is_reference [Boolean] Whether this is a reference variable
76
+ # @return [Hash] Result of setting the variable
77
+ def self.set_variable(service_name, key, value, is_reference:)
78
+ # Skip protected variables
79
+ if protected_variable?(key)
80
+ return {
81
+ success: false,
82
+ skipped: true,
83
+ reason: "Protected system variable (#{key})"
84
+ }
85
+ end
86
+
87
+ # Build the command
88
+ if is_reference
89
+ command = "clackycli variables -s #{shell_escape(service_name)} --set-ref #{shell_escape(key)}=#{shell_escape(value)}"
90
+ else
91
+ command = "clackycli variables -s #{shell_escape(service_name)} --set #{shell_escape(key)}=#{shell_escape(value)}"
92
+ end
93
+
94
+ # Log (with sensitive masking)
95
+ log_value = sensitive_variable?(key) ? '******' : value
96
+ puts "Setting #{key}=#{log_value} on service #{service_name}"
97
+
98
+ # Execute command
99
+ output = `#{command} 2>&1`
100
+ exit_code = $?.exitstatus
101
+
102
+ if exit_code == 0
103
+ { success: true }
104
+ else
105
+ {
106
+ success: false,
107
+ skipped: false,
108
+ error: output.strip
109
+ }
110
+ end
111
+ end
112
+
113
+ # Check if a variable is protected
114
+ #
115
+ # @param key [String] Variable name
116
+ # @return [Boolean] True if protected
117
+ def self.protected_variable?(key)
118
+ PROTECTED_PREFIXES.any? { |prefix| key.start_with?(prefix) }
119
+ end
120
+
121
+ # Check if a variable is sensitive
122
+ #
123
+ # @param key [String] Variable name
124
+ # @return [Boolean] True if sensitive
125
+ def self.sensitive_variable?(key)
126
+ SENSITIVE_PATTERNS.any? { |pattern| key =~ pattern }
127
+ end
128
+
129
+ # Escape shell arguments
130
+ #
131
+ # @param str [String] String to escape
132
+ # @return [String] Escaped string
133
+ def self.shell_escape(str)
134
+ "'#{str.gsub("'", "'\\\\''")}'"
135
+ end
136
+ end
137
+ end
138
+ end
@@ -36,7 +36,7 @@ cd <project_name>
36
36
 
37
37
  ### 5. Success Message
38
38
  Tell user:
39
- - Project created successfully!
39
+ - Project created successfully!
40
40
  - Next step: enter project directory to start development
41
41
  - Command: `cd <project_name>`
42
42
 
@@ -52,4 +52,4 @@ Response:
52
52
  1. Creating a new project named "blog"
53
53
  2. Cloning template...
54
54
  3. Installing dependencies...
55
- 4. Done! You can now: `cd blog` to start development
55
+ 4. Done! You can now: `cd blog` to start development
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+ require_relative "ui_interface"
6
+
7
+ module Clacky
8
+ # JsonUIController implements UIInterface for JSON (NDJSON) output mode.
9
+ # All output is written as one-JSON-per-line to stdout.
10
+ # Confirmation requests read responses from stdin.
11
+ class JsonUIController
12
+ include Clacky::UIInterface
13
+
14
+ def initialize(output: $stdout, input: $stdin)
15
+ @output = output
16
+ @input = input
17
+ @mutex = Mutex.new
18
+ end
19
+
20
+ # Emit a raw NDJSON event
21
+ def emit(type, **data)
22
+ event = { type: type }.merge(data)
23
+ @mutex.synchronize do
24
+ @output.puts(JSON.generate(event))
25
+ @output.flush
26
+ end
27
+ end
28
+
29
+ # === Output display ===
30
+
31
+ def show_user_message(content, images: [])
32
+ emit("user_message", content: content, images: images)
33
+ end
34
+
35
+ def show_assistant_message(content)
36
+ return if content.nil? || content.strip.empty?
37
+
38
+ emit("assistant_message", content: content)
39
+ end
40
+
41
+ def show_tool_call(name, args)
42
+ args_data = args.is_a?(String) ? (JSON.parse(args) rescue args) : args
43
+ emit("tool_call", name: name, args: args_data)
44
+ end
45
+
46
+ def show_tool_result(result)
47
+ emit("tool_result", result: result)
48
+ end
49
+
50
+ def show_tool_error(error)
51
+ error_msg = error.is_a?(Exception) ? error.message : error.to_s
52
+ emit("tool_error", error: error_msg)
53
+ end
54
+
55
+ def show_tool_args(formatted_args)
56
+ emit("tool_args", args: formatted_args)
57
+ end
58
+
59
+ def show_file_write_preview(path, is_new_file:)
60
+ emit("file_preview", path: path, operation: "write", is_new_file: is_new_file)
61
+ end
62
+
63
+ def show_file_edit_preview(path)
64
+ emit("file_preview", path: path, operation: "edit")
65
+ end
66
+
67
+ def show_file_error(error_message)
68
+ emit("file_error", error: error_message)
69
+ end
70
+
71
+ def show_shell_preview(command)
72
+ emit("shell_preview", command: command)
73
+ end
74
+
75
+ def show_diff(old_content, new_content, max_lines: 50)
76
+ emit("diff", old_size: old_content.bytesize, new_size: new_content.bytesize)
77
+ end
78
+
79
+ def show_token_usage(token_data)
80
+ emit("token_usage", **token_data)
81
+ end
82
+
83
+ def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false)
84
+ data = { iterations: iterations, cost: cost }
85
+ data[:duration] = duration if duration
86
+ data[:cache_stats] = cache_stats if cache_stats
87
+ data[:awaiting_user_feedback] = awaiting_user_feedback if awaiting_user_feedback
88
+ emit("complete", **data)
89
+ end
90
+
91
+ # append_output is a no-op in JSON mode (content is already emitted via semantic methods)
92
+ def append_output(content)
93
+ # no-op
94
+ end
95
+
96
+ # === Status messages ===
97
+
98
+ def show_info(message, prefix_newline: true)
99
+ emit("info", message: message)
100
+ end
101
+
102
+ def show_warning(message)
103
+ emit("warning", message: message)
104
+ end
105
+
106
+ def show_error(message)
107
+ emit("error", message: message)
108
+ end
109
+
110
+ def show_success(message)
111
+ emit("success", message: message)
112
+ end
113
+
114
+ def log(message, level: :info)
115
+ emit("log", level: level.to_s, message: message)
116
+ end
117
+
118
+ # === Progress ===
119
+
120
+ def show_progress(message = nil, prefix_newline: true)
121
+ @progress_start_time = Time.now
122
+ emit("progress", message: message, status: "start")
123
+ end
124
+
125
+ def clear_progress
126
+ elapsed = @progress_start_time ? (Time.now - @progress_start_time).round(1) : 0
127
+ @progress_start_time = nil
128
+ emit("progress", status: "stop", elapsed: elapsed)
129
+ end
130
+
131
+ # === State updates ===
132
+
133
+ def update_sessionbar(tasks: nil, cost: nil, status: nil)
134
+ data = {}
135
+ data[:tasks] = tasks if tasks
136
+ data[:cost] = cost if cost
137
+ data[:status] = status if status
138
+ emit("session_update", **data) unless data.empty?
139
+ end
140
+
141
+ def update_todos(todos)
142
+ emit("todo_update", todos: todos)
143
+ end
144
+
145
+ def set_working_status
146
+ emit("session_update", status: "working")
147
+ end
148
+
149
+ def set_idle_status
150
+ emit("session_update", status: "idle")
151
+ end
152
+
153
+ # === Blocking interaction ===
154
+
155
+ def request_confirmation(message, default: true)
156
+ conf_id = "conf_#{SecureRandom.hex(4)}"
157
+ emit("request_confirmation", id: conf_id, message: message, default: default)
158
+
159
+ # Read response from stdin (blocking)
160
+ line = @input.gets
161
+ return default if line.nil?
162
+
163
+ begin
164
+ response = JSON.parse(line.strip)
165
+ result = response["result"] || response[:result]
166
+
167
+ case result.to_s.downcase
168
+ when "yes", "y" then true
169
+ when "no", "n" then false
170
+ else
171
+ # Return as feedback text
172
+ result.to_s
173
+ end
174
+ rescue JSON::ParserError
175
+ default
176
+ end
177
+ end
178
+
179
+ # === Input control (no-ops in JSON mode) ===
180
+
181
+ def clear_input
182
+ # no-op
183
+ end
184
+
185
+ def set_input_tips(message, type: :info)
186
+ # no-op
187
+ end
188
+
189
+ # === Lifecycle ===
190
+
191
+ def stop
192
+ # no-op
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ # Built-in model provider presets
5
+ # Provides default configurations for supported AI model providers
6
+ module Providers
7
+ # Provider preset definitions
8
+ # Each preset includes:
9
+ # - name: Human-readable provider name
10
+ # - base_url: Default API endpoint
11
+ # - api: API type (anthropic-messages, openai-responses, openai-completions)
12
+ # - default_model: Recommended default model
13
+ PRESETS = {
14
+ "anthropic" => {
15
+ "name" => "Anthropic (Claude)",
16
+ "base_url" => "https://api.anthropic.com",
17
+ "api" => "anthropic-messages",
18
+ "default_model" => "claude-sonnet-4-6",
19
+ "models" => ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4"]
20
+ }.freeze,
21
+
22
+ "openrouter" => {
23
+ "name" => "OpenRouter",
24
+ "base_url" => "https://openrouter.ai/api/v1",
25
+ "api" => "openai-responses",
26
+ "default_model" => "anthropic/claude-sonnet-4-5",
27
+ "models" => [] # Dynamic - fetched from API
28
+ }.freeze,
29
+
30
+ "minimax" => {
31
+ "name" => "Minimax",
32
+ "base_url" => "https://api.minimax.chat/v1",
33
+ "api" => "openai-completions",
34
+ "default_model" => "MiniMax-Text-01",
35
+ "models" => ["MiniMax-Text-01", "MiniMax-M2"]
36
+ }.freeze,
37
+
38
+ "kimi" => {
39
+ "name" => "Kimi (Moonshot)",
40
+ "base_url" => "https://api.moonshot.cn/v1",
41
+ "api" => "openai-completions",
42
+ "default_model" => "kimi-k2.5",
43
+ "models" => ["kimi-k2.5"]
44
+ }.freeze
45
+ }.freeze
46
+
47
+ class << self
48
+ # Check if a provider preset exists
49
+ # @param provider_id [String] The provider identifier (e.g., "anthropic", "openrouter")
50
+ # @return [Boolean] True if the preset exists
51
+ def exists?(provider_id)
52
+ PRESETS.key?(provider_id)
53
+ end
54
+
55
+ # Get a provider preset by ID
56
+ # @param provider_id [String] The provider identifier
57
+ # @return [Hash, nil] The preset configuration or nil if not found
58
+ def get(provider_id)
59
+ PRESETS[provider_id]
60
+ end
61
+
62
+ # Get the default model for a provider
63
+ # @param provider_id [String] The provider identifier
64
+ # @return [String, nil] The default model name or nil if provider not found
65
+ def default_model(provider_id)
66
+ preset = PRESETS[provider_id]
67
+ preset&.dig("default_model")
68
+ end
69
+
70
+ # Get the base URL for a provider
71
+ # @param provider_id [String] The provider identifier
72
+ # @return [String, nil] The base URL or nil if provider not found
73
+ def base_url(provider_id)
74
+ preset = PRESETS[provider_id]
75
+ preset&.dig("base_url")
76
+ end
77
+
78
+ # Get the API type for a provider
79
+ # @param provider_id [String] The provider identifier
80
+ # @return [String, nil] The API type or nil if provider not found
81
+ def api_type(provider_id)
82
+ preset = PRESETS[provider_id]
83
+ preset&.dig("api")
84
+ end
85
+
86
+ # List all available provider IDs
87
+ # @return [Array<String>] List of provider identifiers
88
+ def provider_ids
89
+ PRESETS.keys
90
+ end
91
+
92
+ # List all available providers with their names
93
+ # @return [Array<Array(String, String)>] Array of [id, name] pairs
94
+ def list
95
+ PRESETS.map { |id, config| [id, config["name"]] }
96
+ end
97
+
98
+ # Get available models for a provider
99
+ # @param provider_id [String] The provider identifier
100
+ # @return [Array<String>] List of model names (empty if dynamic)
101
+ def models(provider_id)
102
+ preset = PRESETS[provider_id]
103
+ preset&.dig("models") || []
104
+ end
105
+ end
106
+ end
107
+ end