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.
- checksums.yaml +4 -4
- data/.clacky/skills/commit/SKILL.md +29 -4
- data/.clackyrules +3 -1
- data/CHANGELOG.md +103 -2
- data/README.md +70 -161
- data/bin/clarky +11 -0
- data/docs/HOW-TO-USE-CN.md +96 -0
- data/docs/HOW-TO-USE.md +94 -0
- data/docs/config.example.yml +27 -0
- data/docs/deploy_subagent_design.md +540 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/why-openclacky.md +0 -1
- data/lib/clacky/agent/cost_tracker.rb +180 -0
- data/lib/clacky/agent/llm_caller.rb +54 -0
- data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
- data/lib/clacky/agent/message_compressor_helper.rb +534 -0
- data/lib/clacky/agent/session_serializer.rb +152 -0
- data/lib/clacky/agent/skill_manager.rb +138 -0
- data/lib/clacky/agent/system_prompt_builder.rb +96 -0
- data/lib/clacky/agent/time_machine.rb +199 -0
- data/lib/clacky/agent/tool_executor.rb +434 -0
- data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
- data/lib/clacky/agent.rb +260 -1370
- data/lib/clacky/agent_config.rb +447 -10
- data/lib/clacky/cli.rb +275 -98
- data/lib/clacky/client.rb +12 -2
- data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
- data/lib/clacky/default_skills/new/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +195 -0
- data/lib/clacky/providers.rb +107 -0
- data/lib/clacky/skill.rb +48 -7
- data/lib/clacky/skill_loader.rb +7 -0
- data/lib/clacky/tools/edit.rb +105 -48
- data/lib/clacky/tools/file_reader.rb +44 -73
- data/lib/clacky/tools/invoke_skill.rb +89 -0
- data/lib/clacky/tools/list_tasks.rb +54 -0
- data/lib/clacky/tools/redo_task.rb +41 -0
- data/lib/clacky/tools/safe_shell.rb +1 -1
- data/lib/clacky/tools/shell.rb +74 -62
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/undo_task.rb +32 -0
- data/lib/clacky/tools/web_fetch.rb +2 -1
- data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
- data/lib/clacky/ui2/components/inline_input.rb +23 -2
- data/lib/clacky/ui2/components/input_area.rb +65 -21
- data/lib/clacky/ui2/components/modal_component.rb +199 -62
- data/lib/clacky/ui2/layout_manager.rb +75 -25
- data/lib/clacky/ui2/line_editor.rb +23 -2
- data/lib/clacky/ui2/markdown_renderer.rb +31 -10
- data/lib/clacky/ui2/screen_buffer.rb +2 -0
- data/lib/clacky/ui2/ui_controller.rb +316 -37
- data/lib/clacky/ui2.rb +2 -0
- data/lib/clacky/ui_interface.rb +50 -0
- data/lib/clacky/utils/arguments_parser.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -18
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +19 -9
- data/scripts/install.sh +274 -97
- data/scripts/uninstall.sh +12 -12
- metadata +40 -13
- data/.clacky/skills/test-skill/SKILL.md +0 -15
- data/lib/clacky/compression/base.rb +0 -231
- data/lib/clacky/compression/standard.rb +0 -339
- data/lib/clacky/config.rb +0 -117
- /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
- /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
- /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
- /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
- /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
- /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
|
-
-
|
|
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.
|
|
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
|