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
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 [MESSAGE]", "Run agent in interactive mode with autonomous tool use (default)"
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, confirm_edits, plan_only"
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(message = nil)
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
- config = Clacky::Config.load
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
- # Build agent config
75
- agent_config = build_agent_config(config)
76
- client = Clacky::Client.new(config.api_key, base_url: config.base_url, anthropic_format: config.anthropic_format?)
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
- run_agent_with_ui2(agent, working_dir, agent_config, message, session_manager, client, is_session_load: is_session_load)
101
- rescue StandardError => e
102
- # Save session on error
103
- if session_manager
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 = Clacky::Config.load
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.model)
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 cancelled, return early
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
- # Update config with non-empty values
153
- config.api_key = result[:api_key] unless result[:api_key].to_s.empty?
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 agent config model
165
- agent_config.model = config.model
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 saved successfully!")
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 build_agent_config(config)
177
- AgentConfig.new(
178
- model: options[:model] || config.model,
179
- permission_mode: options[:mode].to_sym,
180
- verbose: options[:verbose]
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
- first_msg = session[:first_user_message] || "No message"
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
- first_msg = session[:first_user_message] || "No message"
276
- say " #{idx + 1}. [#{session_id}] #{created_at} - #{first_msg}", :cyan
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.stop_progress_thread
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, initial_message = nil, session_manager = nil, client = nil, is_session_load: false)
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.model,
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 agent thread state
329
- agent_thread = nil
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 agent_thread&.alive?) && input_was_empty
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 agent_thread&.alive?
364
- agent_thread.raise(Clacky::AgentInterrupted, "User interrupted")
491
+ if current_task_thread&.alive?
492
+ current_task_thread.raise(Clacky::AgentInterrupted, "User interrupted")
365
493
  end
366
- ui_controller.input_area.clear
367
- ui_controller.input_area.set_tips("Press Ctrl+C again to exit.", type: :info)
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 agent is already running, interrupt it first
395
- if agent_thread&.alive?
396
- agent_thread.raise(Clacky::AgentInterrupted, "New input received")
397
- agent_thread.join(2) # Wait up to 2 seconds for graceful shutdown
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
- agent_thread = Thread.new do
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
- agent_thread = nil
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
- raw_body
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.