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
data/lib/clacky/cli.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require "tty-prompt"
|
|
5
5
|
require_relative "ui2"
|
|
6
|
+
require_relative "json_ui_controller"
|
|
6
7
|
|
|
7
8
|
module Clacky
|
|
8
9
|
class CLI < Thor
|
|
@@ -13,7 +14,7 @@ module Clacky
|
|
|
13
14
|
# Set agent as the default command
|
|
14
15
|
default_task :agent
|
|
15
16
|
|
|
16
|
-
desc "agent
|
|
17
|
+
desc "agent", "Run agent in interactive mode with autonomous tool use (default)"
|
|
17
18
|
long_desc <<-LONGDESC
|
|
18
19
|
Run an AI agent in interactive mode that can autonomously use tools to complete tasks.
|
|
19
20
|
|
|
@@ -24,7 +25,6 @@ module Clacky
|
|
|
24
25
|
Permission modes:
|
|
25
26
|
auto_approve - Automatically execute all tools (use with caution)
|
|
26
27
|
confirm_safes - Auto-approve safe operations, confirm risky ones (default)
|
|
27
|
-
confirm_edits - Auto-approve read-only tools, confirm edits
|
|
28
28
|
plan_only - Generate plan without executing
|
|
29
29
|
|
|
30
30
|
UI themes:
|
|
@@ -40,7 +40,7 @@ module Clacky
|
|
|
40
40
|
$ clacky agent --mode=auto_approve --path /path/to/project
|
|
41
41
|
LONGDESC
|
|
42
42
|
option :mode, type: :string, default: "confirm_safes",
|
|
43
|
-
desc: "Permission mode: auto_approve, confirm_safes,
|
|
43
|
+
desc: "Permission mode: auto_approve, confirm_safes, plan_only"
|
|
44
44
|
option :theme, type: :string, default: "hacker",
|
|
45
45
|
desc: "UI theme: hacker, minimal (default: hacker)"
|
|
46
46
|
option :verbose, type: :boolean, aliases: "-v", default: false, desc: "Show detailed output"
|
|
@@ -48,14 +48,15 @@ module Clacky
|
|
|
48
48
|
option :continue, type: :boolean, aliases: "-c", desc: "Continue most recent session"
|
|
49
49
|
option :list, type: :boolean, aliases: "-l", desc: "List recent sessions"
|
|
50
50
|
option :attach, type: :string, aliases: "-a", desc: "Attach to session by number or keyword"
|
|
51
|
+
option :json, type: :boolean, default: false, desc: "Output NDJSON to stdout (for scripting/piping)"
|
|
51
52
|
option :help, type: :boolean, aliases: "-h", desc: "Show this help message"
|
|
52
|
-
def agent
|
|
53
|
+
def agent
|
|
53
54
|
# Handle help option
|
|
54
55
|
if options[:help]
|
|
55
56
|
invoke :help, ["agent"]
|
|
56
57
|
return
|
|
57
58
|
end
|
|
58
|
-
|
|
59
|
+
agent_config = Clacky::AgentConfig.load
|
|
59
60
|
|
|
60
61
|
# Handle session listing
|
|
61
62
|
if options[:list]
|
|
@@ -71,9 +72,12 @@ module Clacky
|
|
|
71
72
|
# Validate and get working directory
|
|
72
73
|
working_dir = validate_working_directory(options[:path])
|
|
73
74
|
|
|
74
|
-
#
|
|
75
|
-
agent_config =
|
|
76
|
-
|
|
75
|
+
# Update agent config with CLI options
|
|
76
|
+
agent_config.permission_mode = options[:mode].to_sym if options[:mode]
|
|
77
|
+
agent_config.verbose = options[:verbose] if options[:verbose]
|
|
78
|
+
|
|
79
|
+
# Create client for current model
|
|
80
|
+
client = Clacky::Client.new(agent_config.api_key, base_url: agent_config.base_url, anthropic_format: agent_config.anthropic_format?)
|
|
77
81
|
|
|
78
82
|
# Handle session loading/continuation
|
|
79
83
|
session_manager = Clacky::SessionManager.new
|
|
@@ -95,90 +99,126 @@ module Clacky
|
|
|
95
99
|
original_dir = Dir.pwd
|
|
96
100
|
should_chdir = File.realpath(working_dir) != File.realpath(original_dir)
|
|
97
101
|
Dir.chdir(working_dir) if should_chdir
|
|
98
|
-
|
|
99
102
|
begin
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
session_manager.save(agent.to_session_data(status: :error, error_message: e.message))
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Report the error
|
|
108
|
-
say "\n❌ Error: #{e.message}", :red
|
|
109
|
-
say e.backtrace.first(5).join("\n"), :red if options[:verbose]
|
|
110
|
-
|
|
111
|
-
# Show session saved message
|
|
112
|
-
if session_manager&.last_saved_path
|
|
113
|
-
say "\n📂 Session saved: #{session_manager.last_saved_path}", :yellow
|
|
103
|
+
if options[:json]
|
|
104
|
+
run_agent_with_json(agent, working_dir, agent_config, session_manager, client)
|
|
105
|
+
else
|
|
106
|
+
run_agent_with_ui2(agent, working_dir, agent_config, session_manager, client, is_session_load: is_session_load)
|
|
114
107
|
end
|
|
115
|
-
|
|
116
|
-
# Guide user to recover
|
|
117
|
-
say "\n💡 To recover and retry, run:", :yellow
|
|
118
|
-
say " clacky agent -c", :cyan
|
|
119
|
-
|
|
120
|
-
exit 1
|
|
121
108
|
ensure
|
|
122
109
|
Dir.chdir(original_dir)
|
|
123
110
|
end
|
|
124
111
|
end
|
|
125
112
|
|
|
126
113
|
no_commands do
|
|
127
|
-
private def handle_config_command(ui_controller, client, agent_config)
|
|
128
|
-
config =
|
|
114
|
+
private def handle_config_command(ui_controller, client, agent_config, agent)
|
|
115
|
+
config = agent_config
|
|
129
116
|
|
|
130
117
|
# Create test callback
|
|
131
118
|
test_callback = lambda do |test_config|
|
|
132
119
|
# Create a temporary client with new config to test
|
|
133
120
|
test_client = Clacky::Client.new(
|
|
134
|
-
test_config.api_key,
|
|
135
|
-
base_url: test_config.base_url,
|
|
121
|
+
test_config.api_key,
|
|
122
|
+
base_url: test_config.base_url,
|
|
136
123
|
anthropic_format: test_config.anthropic_format?
|
|
137
124
|
)
|
|
138
|
-
|
|
125
|
+
|
|
139
126
|
# Test connection
|
|
140
|
-
test_client.test_connection(model: test_config.
|
|
127
|
+
test_client.test_connection(model: test_config.model_name)
|
|
141
128
|
end
|
|
142
129
|
|
|
143
130
|
# Show modal dialog for configuration with test callback
|
|
144
131
|
result = ui_controller.show_config_modal(config, test_callback: test_callback)
|
|
145
132
|
|
|
146
|
-
# If user
|
|
133
|
+
# If user closed modal without changes, return early
|
|
147
134
|
if result.nil?
|
|
148
|
-
ui_controller.show_warning("Configuration cancelled")
|
|
149
135
|
return
|
|
150
136
|
end
|
|
151
137
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
config.model = result[:model] unless result[:model].to_s.empty?
|
|
155
|
-
config.base_url = result[:base_url] unless result[:base_url].to_s.empty?
|
|
156
|
-
|
|
157
|
-
# Save configuration (only reached if test passed)
|
|
158
|
-
config.save
|
|
159
|
-
|
|
160
|
-
# Update client with new config
|
|
138
|
+
# Config was changed (either switch or edit), update client, agent, and UI
|
|
139
|
+
# Update client with current model's config
|
|
161
140
|
client.instance_variable_set(:@api_key, config.api_key)
|
|
162
141
|
client.instance_variable_set(:@base_url, config.base_url)
|
|
142
|
+
client.instance_variable_set(:@use_anthropic_format, config.anthropic_format?)
|
|
143
|
+
|
|
144
|
+
# Update agent's client (agent has its own @client instance variable)
|
|
145
|
+
agent.instance_variable_set(:@client, Clacky::Client.new(
|
|
146
|
+
config.api_key,
|
|
147
|
+
base_url: config.base_url,
|
|
148
|
+
anthropic_format: config.anthropic_format?
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
# Update agent's message compressor with new client
|
|
152
|
+
agent.instance_variable_set(:@message_compressor,
|
|
153
|
+
Clacky::MessageCompressor.new(agent.instance_variable_get(:@client), model: config.model_name)
|
|
154
|
+
)
|
|
163
155
|
|
|
164
|
-
# Update
|
|
165
|
-
|
|
156
|
+
# Update UI controller's model display
|
|
157
|
+
ui_controller.config[:model] = config.model_name
|
|
158
|
+
ui_controller.update_sessionbar(
|
|
159
|
+
tasks: agent.total_tasks,
|
|
160
|
+
cost: agent.total_cost
|
|
161
|
+
)
|
|
166
162
|
|
|
167
163
|
# Show success message in output
|
|
168
164
|
masked_key = "#{config.api_key[0..7]}#{'*' * 20}#{config.api_key[-4..]}"
|
|
169
|
-
ui_controller.show_success("Configuration
|
|
165
|
+
ui_controller.show_success("Configuration updated!")
|
|
166
|
+
ui_controller.append_output(" Current Model: #{config.model_name}")
|
|
170
167
|
ui_controller.append_output(" API Key: #{masked_key}")
|
|
171
|
-
ui_controller.append_output(" Model: #{config.model}")
|
|
172
168
|
ui_controller.append_output(" Base URL: #{config.base_url}")
|
|
169
|
+
ui_controller.append_output(" Format: #{config.anthropic_format? ? 'Anthropic' : 'OpenAI'}")
|
|
173
170
|
ui_controller.append_output("")
|
|
174
171
|
end
|
|
175
172
|
|
|
176
|
-
private def
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
173
|
+
private def handle_time_machine_command(ui_controller, agent, session_manager)
|
|
174
|
+
# Get task history from agent
|
|
175
|
+
history = agent.get_task_history(limit: 10)
|
|
176
|
+
|
|
177
|
+
if history.empty?
|
|
178
|
+
ui_controller.show_info("No task history available yet.")
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Show time machine menu
|
|
183
|
+
selected_task_id = ui_controller.show_time_machine_menu(history)
|
|
184
|
+
|
|
185
|
+
# If user cancelled, return
|
|
186
|
+
return if selected_task_id.nil?
|
|
187
|
+
|
|
188
|
+
# Get current active task for comparison
|
|
189
|
+
current_task_id = agent.instance_variable_get(:@active_task_id)
|
|
190
|
+
|
|
191
|
+
# Perform the switch
|
|
192
|
+
begin
|
|
193
|
+
if selected_task_id < current_task_id
|
|
194
|
+
# Undo to selected task
|
|
195
|
+
ui_controller.show_info("Undoing to Task #{selected_task_id}...")
|
|
196
|
+
result = agent.switch_to_task(selected_task_id)
|
|
197
|
+
if result[:success]
|
|
198
|
+
ui_controller.show_success("✓ #{result[:message]}")
|
|
199
|
+
else
|
|
200
|
+
ui_controller.show_error(result[:message])
|
|
201
|
+
return
|
|
202
|
+
end
|
|
203
|
+
else
|
|
204
|
+
# Redo to selected task
|
|
205
|
+
ui_controller.show_info("Redoing to Task #{selected_task_id}...")
|
|
206
|
+
result = agent.switch_to_task(selected_task_id)
|
|
207
|
+
if result[:success]
|
|
208
|
+
ui_controller.show_success("✓ #{result[:message]}")
|
|
209
|
+
else
|
|
210
|
+
ui_controller.show_error(result[:message])
|
|
211
|
+
return
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Save session after switch
|
|
216
|
+
if session_manager
|
|
217
|
+
session_manager.save(agent.to_session_data(status: :success))
|
|
218
|
+
end
|
|
219
|
+
rescue StandardError => e
|
|
220
|
+
ui_controller.show_error("Time Machine failed: #{e.message}")
|
|
221
|
+
end
|
|
182
222
|
end
|
|
183
223
|
|
|
184
224
|
def validate_working_directory(path)
|
|
@@ -218,7 +258,7 @@ module Clacky
|
|
|
218
258
|
session_id = session[:session_id][0..7]
|
|
219
259
|
tasks = session.dig(:stats, :total_tasks) || 0
|
|
220
260
|
cost = session.dig(:stats, :total_cost_usd) || 0.0
|
|
221
|
-
|
|
261
|
+
last_msg = session[:last_user_message] || "No message"
|
|
222
262
|
is_current_dir = session[:working_dir] == working_dir
|
|
223
263
|
|
|
224
264
|
dir_marker = is_current_dir ? "📍" : " "
|
|
@@ -272,8 +312,8 @@ module Clacky
|
|
|
272
312
|
matching_sessions.each_with_index do |session, idx|
|
|
273
313
|
created_at = Time.parse(session[:created_at]).strftime("%Y-%m-%d %H:%M")
|
|
274
314
|
session_id = session[:session_id][0..7]
|
|
275
|
-
|
|
276
|
-
say " #{idx + 1}. [#{session_id}] #{created_at} - #{
|
|
315
|
+
last_msg = session[:last_user_message] || "No message"
|
|
316
|
+
say " #{idx + 1}. [#{session_id}] #{created_at} - #{last_msg}", :cyan
|
|
277
317
|
end
|
|
278
318
|
say "\nPlease use a more specific prefix.", :yellow
|
|
279
319
|
exit 1
|
|
@@ -288,7 +328,7 @@ module Clacky
|
|
|
288
328
|
|
|
289
329
|
# Handle agent error/interrupt with cleanup
|
|
290
330
|
def handle_agent_exception(ui_controller, agent, session_manager, exception)
|
|
291
|
-
ui_controller.
|
|
331
|
+
ui_controller.clear_progress
|
|
292
332
|
ui_controller.set_idle_status
|
|
293
333
|
|
|
294
334
|
if exception.is_a?(Clacky::AgentInterrupted)
|
|
@@ -301,8 +341,89 @@ module Clacky
|
|
|
301
341
|
end
|
|
302
342
|
end
|
|
303
343
|
|
|
344
|
+
# Run agent with JSON (NDJSON) output mode — persistent process.
|
|
345
|
+
# Reads JSON messages from stdin, writes NDJSON events to stdout.
|
|
346
|
+
# Stays alive until "/exit", {"type":"exit"}, or stdin EOF.
|
|
347
|
+
#
|
|
348
|
+
# Input protocol (one JSON per line on stdin):
|
|
349
|
+
# {"type":"message","content":"..."} — run agent with this message
|
|
350
|
+
# {"type":"message","content":"...","images":["path"]} — with images
|
|
351
|
+
# {"type":"exit"} — graceful shutdown
|
|
352
|
+
# {"type":"confirmation","id":"conf_1","result":"yes"} — answer to request_confirmation
|
|
353
|
+
#
|
|
354
|
+
# If a bare string line is received it is treated as a message content.
|
|
355
|
+
def run_agent_with_json(agent, working_dir, agent_config, session_manager, client)
|
|
356
|
+
json_ui = Clacky::JsonUIController.new
|
|
357
|
+
agent.instance_variable_set(:@ui, json_ui)
|
|
358
|
+
|
|
359
|
+
json_ui.emit("system", message: "Agent started", model: agent_config.model_name, working_dir: working_dir)
|
|
360
|
+
|
|
361
|
+
# Persistent input loop — read JSON lines from stdin
|
|
362
|
+
while (line = $stdin.gets)
|
|
363
|
+
line = line.strip
|
|
364
|
+
next if line.empty?
|
|
365
|
+
|
|
366
|
+
# Parse input
|
|
367
|
+
input = begin
|
|
368
|
+
JSON.parse(line)
|
|
369
|
+
rescue JSON::ParserError
|
|
370
|
+
# Treat bare string as a message
|
|
371
|
+
{ "type" => "message", "content" => line }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
type = input["type"] || "message"
|
|
375
|
+
|
|
376
|
+
case type
|
|
377
|
+
when "message"
|
|
378
|
+
content = input["content"].to_s.strip
|
|
379
|
+
if content.empty?
|
|
380
|
+
json_ui.emit("error", message: "Empty message content")
|
|
381
|
+
next
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Handle built-in commands
|
|
385
|
+
case content.downcase
|
|
386
|
+
when "/exit", "/quit"
|
|
387
|
+
break
|
|
388
|
+
when "/clear"
|
|
389
|
+
agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir)
|
|
390
|
+
agent.instance_variable_set(:@ui, json_ui)
|
|
391
|
+
json_ui.emit("info", message: "Session cleared. Starting fresh.")
|
|
392
|
+
next
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
images = input["images"] || []
|
|
396
|
+
run_json_task(agent, json_ui, session_manager) { agent.run(content, images: images) }
|
|
397
|
+
when "exit"
|
|
398
|
+
break
|
|
399
|
+
else
|
|
400
|
+
json_ui.emit("error", message: "Unknown input type: #{type}")
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Final session save and shutdown
|
|
405
|
+
if session_manager && agent.total_tasks > 0
|
|
406
|
+
session_manager.save(agent.to_session_data(status: :exited))
|
|
407
|
+
end
|
|
408
|
+
json_ui.emit("done", total_cost: agent.total_cost, total_tasks: agent.total_tasks)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Execute a single agent task inside the JSON loop, with error handling.
|
|
412
|
+
def run_json_task(agent, json_ui, session_manager)
|
|
413
|
+
json_ui.set_working_status
|
|
414
|
+
yield
|
|
415
|
+
session_manager&.save(agent.to_session_data(status: :success))
|
|
416
|
+
json_ui.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
417
|
+
rescue Clacky::AgentInterrupted
|
|
418
|
+
json_ui.emit("interrupted")
|
|
419
|
+
rescue => e
|
|
420
|
+
json_ui.emit("error", message: e.message)
|
|
421
|
+
ensure
|
|
422
|
+
json_ui.set_idle_status
|
|
423
|
+
end
|
|
424
|
+
|
|
304
425
|
# Run agent with UI2 split-screen interface
|
|
305
|
-
def run_agent_with_ui2(agent, working_dir, agent_config,
|
|
426
|
+
def run_agent_with_ui2(agent, working_dir, agent_config, session_manager = nil, client = nil, is_session_load: false)
|
|
306
427
|
# Validate theme
|
|
307
428
|
theme_name = options[:theme] || "hacker"
|
|
308
429
|
available_themes = UI2::ThemeManager.available_themes.map(&:to_s)
|
|
@@ -315,7 +436,7 @@ module Clacky
|
|
|
315
436
|
ui_controller = UI2::UIController.new(
|
|
316
437
|
working_dir: working_dir,
|
|
317
438
|
mode: agent_config.permission_mode.to_s,
|
|
318
|
-
model: agent_config.
|
|
439
|
+
model: agent_config.model_name,
|
|
319
440
|
theme: theme_name
|
|
320
441
|
)
|
|
321
442
|
|
|
@@ -325,17 +446,24 @@ module Clacky
|
|
|
325
446
|
# Set skill loader for command suggestions
|
|
326
447
|
ui_controller.set_skill_loader(agent.skill_loader)
|
|
327
448
|
|
|
328
|
-
# Track
|
|
329
|
-
|
|
449
|
+
# Track current working thread (agent or idle compression that can be interrupted)
|
|
450
|
+
# idle_timer is tracked separately because it should not be interrupted during sleep
|
|
451
|
+
current_task_thread = nil
|
|
452
|
+
idle_timer_thread = nil
|
|
330
453
|
|
|
331
454
|
# Set up mode toggle handler
|
|
332
455
|
ui_controller.on_mode_toggle do |new_mode|
|
|
333
456
|
agent_config.permission_mode = new_mode.to_sym
|
|
334
457
|
end
|
|
335
458
|
|
|
459
|
+
# Set up time machine handler (ESC key)
|
|
460
|
+
ui_controller.on_time_machine do
|
|
461
|
+
handle_time_machine_command(ui_controller, agent, session_manager)
|
|
462
|
+
end
|
|
463
|
+
|
|
336
464
|
# Set up interrupt handler
|
|
337
465
|
ui_controller.on_interrupt do |input_was_empty:|
|
|
338
|
-
if (not
|
|
466
|
+
if (not current_task_thread&.alive?) && input_was_empty
|
|
339
467
|
# Save final session state before exit
|
|
340
468
|
if session_manager && agent.total_tasks > 0
|
|
341
469
|
session_data = agent.to_session_data(status: :exited)
|
|
@@ -360,21 +488,27 @@ module Clacky
|
|
|
360
488
|
exit(0)
|
|
361
489
|
end
|
|
362
490
|
|
|
363
|
-
if
|
|
364
|
-
|
|
491
|
+
if current_task_thread&.alive?
|
|
492
|
+
current_task_thread.raise(Clacky::AgentInterrupted, "User interrupted")
|
|
365
493
|
end
|
|
366
|
-
ui_controller.
|
|
367
|
-
ui_controller.
|
|
494
|
+
ui_controller.clear_input
|
|
495
|
+
ui_controller.set_input_tips("Press Ctrl+C again to exit.", type: :info)
|
|
368
496
|
end
|
|
369
497
|
|
|
370
498
|
# Set up input handler
|
|
371
|
-
ui_controller.on_input do |input, images|
|
|
499
|
+
ui_controller.on_input do |input, images, display: nil|
|
|
372
500
|
# Handle commands
|
|
373
501
|
case input.downcase.strip
|
|
374
502
|
when "/config"
|
|
375
|
-
handle_config_command(ui_controller, client, agent_config)
|
|
503
|
+
handle_config_command(ui_controller, client, agent_config, agent)
|
|
504
|
+
next
|
|
505
|
+
when "/undo"
|
|
506
|
+
handle_time_machine_command(ui_controller, agent, session_manager)
|
|
376
507
|
next
|
|
377
508
|
when "/clear"
|
|
509
|
+
sleep 0.1
|
|
510
|
+
# Clear output area
|
|
511
|
+
ui_controller.layout.clear_output
|
|
378
512
|
# Clear session by creating a new agent
|
|
379
513
|
agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: ui_controller)
|
|
380
514
|
ui_controller.show_info("Session cleared. Starting fresh.")
|
|
@@ -387,18 +521,75 @@ module Clacky
|
|
|
387
521
|
ui_controller.stop
|
|
388
522
|
exit(0)
|
|
389
523
|
when "/help"
|
|
524
|
+
sleep 0.1
|
|
390
525
|
ui_controller.show_help
|
|
391
526
|
next
|
|
392
527
|
end
|
|
393
528
|
|
|
394
|
-
# If
|
|
395
|
-
if
|
|
396
|
-
|
|
397
|
-
|
|
529
|
+
# If any task thread is running, interrupt it first
|
|
530
|
+
if current_task_thread&.alive?
|
|
531
|
+
current_task_thread.raise(Clacky::AgentInterrupted, "New input received")
|
|
532
|
+
current_task_thread.join(2) # Wait up to 2 seconds for graceful shutdown
|
|
533
|
+
ui_controller.set_idle_status
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Cancel idle timer if running (new input means user is active)
|
|
537
|
+
if idle_timer_thread&.alive?
|
|
538
|
+
ui_controller.log("Idle timer killed, start new 1", level: :debug)
|
|
539
|
+
idle_timer_thread.kill
|
|
540
|
+
idle_timer_thread = nil
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Helper method to start idle timer after agent completes
|
|
544
|
+
start_idle_timer = lambda do
|
|
545
|
+
# Cancel any existing idle timer first
|
|
546
|
+
if idle_timer_thread&.alive?
|
|
547
|
+
ui_controller.log("Idle timer killed, start new 2", level: :debug)
|
|
548
|
+
idle_timer_thread.kill
|
|
549
|
+
idle_timer_thread = nil
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Start idle timer - trigger compression after 180 seconds of inactivity
|
|
553
|
+
idle_timer_thread = Thread.new do
|
|
554
|
+
ui_controller.log("Idle timer started, will trigger compression in 180 seconds", level: :debug)
|
|
555
|
+
# Sleep outside of rescue block - if interrupted here, let it propagate and exit
|
|
556
|
+
sleep 180
|
|
557
|
+
ui_controller.log("Idle timer sleep completed, starting compression", level: :debug)
|
|
558
|
+
|
|
559
|
+
# After sleep completes, switch to current_task_thread for compression
|
|
560
|
+
# (so it can be interrupted by Ctrl+C)
|
|
561
|
+
current_task_thread = Thread.new do
|
|
562
|
+
begin
|
|
563
|
+
# After 60 seconds, start idle compression
|
|
564
|
+
ui_controller.set_working_status
|
|
565
|
+
success = agent.trigger_idle_compression
|
|
566
|
+
|
|
567
|
+
if success
|
|
568
|
+
# Update session bar after compression
|
|
569
|
+
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
570
|
+
# Save session after compression
|
|
571
|
+
session_manager&.save(agent.to_session_data(status: :success))
|
|
572
|
+
end
|
|
573
|
+
rescue Clacky::AgentInterrupted
|
|
574
|
+
# Compression was interrupted by user
|
|
575
|
+
ui_controller.append_output("")
|
|
576
|
+
ui_controller.show_info("Idle compression cancelled")
|
|
577
|
+
rescue => e
|
|
578
|
+
ui_controller.log("Idle compression error: #{e.message}", level: :error)
|
|
579
|
+
ensure
|
|
580
|
+
ui_controller.set_idle_status
|
|
581
|
+
current_task_thread = nil
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Wait for compression to complete
|
|
586
|
+
current_task_thread.join
|
|
587
|
+
idle_timer_thread = nil
|
|
588
|
+
end
|
|
398
589
|
end
|
|
399
590
|
|
|
400
591
|
# Run agent in background thread
|
|
401
|
-
|
|
592
|
+
current_task_thread = Thread.new do
|
|
402
593
|
begin
|
|
403
594
|
# Set status to working when agent starts
|
|
404
595
|
ui_controller.set_working_status
|
|
@@ -417,7 +608,9 @@ module Clacky
|
|
|
417
608
|
rescue Clacky::AgentInterrupted, StandardError => e
|
|
418
609
|
handle_agent_exception(ui_controller, agent, session_manager, e)
|
|
419
610
|
ensure
|
|
420
|
-
|
|
611
|
+
current_task_thread = nil
|
|
612
|
+
# Start idle timer after agent completes
|
|
613
|
+
start_idle_timer.call
|
|
421
614
|
end
|
|
422
615
|
end
|
|
423
616
|
end
|
|
@@ -432,36 +625,20 @@ module Clacky
|
|
|
432
625
|
ui_controller.initialize_and_show_banner
|
|
433
626
|
end
|
|
434
627
|
|
|
435
|
-
# If there's an initial message, process it
|
|
436
|
-
if initial_message && !initial_message.strip.empty?
|
|
437
|
-
ui_controller.show_user_message(initial_message)
|
|
438
|
-
|
|
439
|
-
begin
|
|
440
|
-
# Set status to working when agent starts
|
|
441
|
-
ui_controller.set_working_status
|
|
442
|
-
|
|
443
|
-
result = agent.run(initial_message, images: [])
|
|
444
|
-
|
|
445
|
-
if session_manager
|
|
446
|
-
session_manager.save(agent.to_session_data(status: :success))
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
# Update session bar with agent's cumulative stats
|
|
450
|
-
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
451
|
-
rescue Clacky::AgentInterrupted, StandardError => e
|
|
452
|
-
handle_agent_exception(ui_controller, agent, session_manager, e)
|
|
453
|
-
end
|
|
454
|
-
end
|
|
455
|
-
|
|
456
628
|
# Start input loop (blocks until exit)
|
|
457
629
|
ui_controller.start_input_loop
|
|
458
630
|
|
|
631
|
+
# Cleanup: kill any running thread
|
|
632
|
+
current_task_thread&.kill
|
|
633
|
+
|
|
459
634
|
# Save final session state
|
|
460
635
|
if session_manager && agent.total_tasks > 0
|
|
461
636
|
session_manager.save(agent.to_session_data)
|
|
462
637
|
end
|
|
463
638
|
end
|
|
464
639
|
|
|
640
|
+
|
|
641
|
+
|
|
465
642
|
end
|
|
466
643
|
end
|
|
467
644
|
end
|
data/lib/clacky/client.rb
CHANGED
|
@@ -54,7 +54,11 @@ module Clacky
|
|
|
54
54
|
end
|
|
55
55
|
handle_test_response(response)
|
|
56
56
|
end
|
|
57
|
+
rescue Faraday::Error => e
|
|
58
|
+
# Network or connection errors
|
|
59
|
+
{ success: false, error: "Connection error: #{e.message}" }
|
|
57
60
|
rescue => e
|
|
61
|
+
# Other errors
|
|
58
62
|
{ success: false, error: e.message }
|
|
59
63
|
end
|
|
60
64
|
|
|
@@ -752,6 +756,11 @@ module Clacky
|
|
|
752
756
|
|
|
753
757
|
# Extract the most meaningful error message from API response
|
|
754
758
|
private def extract_error_message(error_body, raw_body)
|
|
759
|
+
# Check if response is HTML (indicates wrong endpoint or server error)
|
|
760
|
+
if raw_body.is_a?(String) && raw_body.strip.start_with?('<!DOCTYPE', '<html')
|
|
761
|
+
return "Invalid API endpoint or server error (received HTML instead of JSON)"
|
|
762
|
+
end
|
|
763
|
+
|
|
755
764
|
return raw_body unless error_body.is_a?(Hash)
|
|
756
765
|
|
|
757
766
|
# Priority order for error messages:
|
|
@@ -759,7 +768,7 @@ module Clacky
|
|
|
759
768
|
# 2. error.message (Anthropic format)
|
|
760
769
|
# 3. message
|
|
761
770
|
# 4. error (string)
|
|
762
|
-
# 5. raw body
|
|
771
|
+
# 5. raw body (truncated if too long)
|
|
763
772
|
if error_body["upstreamMessage"] && !error_body["upstreamMessage"].empty?
|
|
764
773
|
error_body["upstreamMessage"]
|
|
765
774
|
elsif error_body.dig("error", "message")
|
|
@@ -769,7 +778,8 @@ module Clacky
|
|
|
769
778
|
elsif error_body["error"].is_a?(String)
|
|
770
779
|
error_body["error"]
|
|
771
780
|
else
|
|
772
|
-
|
|
781
|
+
# Truncate raw body if too long
|
|
782
|
+
raw_body.is_a?(String) && raw_body.length > 200 ? "#{raw_body[0..200]}..." : raw_body
|
|
773
783
|
end
|
|
774
784
|
end
|
|
775
785
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-explorer
|
|
3
|
+
description: Use this skill when exploring, analyzing, or understanding project/code structure. Required for tasks like "analyze project", "explore codebase", "understand how X works".
|
|
4
|
+
fork_agent: true
|
|
5
|
+
forbidden_tools:
|
|
6
|
+
- write
|
|
7
|
+
- edit
|
|
8
|
+
auto_summarize: true
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Code Explorer Subagent
|
|
12
|
+
|
|
13
|
+
You are now running in a **forked subagent** mode optimized for fast code exploration.
|
|
14
|
+
|
|
15
|
+
## Your Mission
|
|
16
|
+
Quickly explore and analyze the codebase to answer questions or gather information.
|
|
17
|
+
|
|
18
|
+
## Your Restrictions
|
|
19
|
+
- NO modifications: You CANNOT use `write` or `edit` tools
|
|
20
|
+
- Read-only: Your role is to ANALYZE, not to change
|
|
21
|
+
|
|
22
|
+
## Workflow — follow this order strictly
|
|
23
|
+
|
|
24
|
+
1. **List the file tree** — run `glob` with `**/*` to get an overview of the project structure
|
|
25
|
+
2. **Read README.md** — if it exists, read it to understand the project purpose and layout
|
|
26
|
+
3. **Find relevant files** — based on the task, use `grep` to locate key patterns or specific files
|
|
27
|
+
4. **Read only what's needed** — use `file_reader` only on the files directly relevant to the question
|
|
28
|
+
5. **Report clearly** — provide a concise, actionable summary
|
|
29
|
+
|
|
30
|
+
## Rules
|
|
31
|
+
- Do NOT read files blindly — always have a reason before opening a file
|
|
32
|
+
- Do NOT read every file in a directory — be selective
|
|
33
|
+
- Prefer `grep` over `file_reader` for finding specific patterns
|
|
34
|
+
- Stop as soon as you have enough information to answer the question
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy
|
|
3
|
+
description: Deploy Rails applications to Railway PaaS
|
|
4
|
+
fork_agent: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Railway Deployment for Rails
|
|
8
|
+
|
|
9
|
+
Deploy a Rails application to Railway platform.
|
|
10
|
+
|
|
11
|
+
Execute the Rails deployment script located in this skill's `scripts/rails_deploy.rb`.
|
|
12
|
+
|
|
13
|
+
The script validates the environment and runs an 8-step deployment workflow.
|