openclacky 0.5.1 → 0.5.3
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/.clackyrules +2 -0
- data/README.md +1 -0
- data/lib/clacky/agent.rb +121 -17
- data/lib/clacky/agent_config.rb +4 -1
- data/lib/clacky/cli.rb +119 -93
- data/lib/clacky/client.rb +56 -7
- data/lib/clacky/hook_manager.rb +2 -1
- data/lib/clacky/progress_indicator.rb +13 -10
- data/lib/clacky/tools/safe_shell.rb +7 -2
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/ui/banner.rb +144 -0
- data/lib/clacky/ui/formatter.rb +209 -0
- data/lib/clacky/ui/prompt.rb +72 -0
- data/lib/clacky/ui/statusbar.rb +98 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +6 -0
- metadata +33 -1
data/lib/clacky/cli.rb
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require "tty-prompt"
|
|
5
5
|
require "tty-spinner"
|
|
6
|
-
|
|
6
|
+
require_relative "ui/banner"
|
|
7
|
+
require_relative "ui/prompt"
|
|
8
|
+
require_relative "ui/statusbar"
|
|
9
|
+
require_relative "ui/formatter"
|
|
7
10
|
|
|
8
11
|
module Clacky
|
|
9
12
|
class CLI < Thor
|
|
@@ -140,7 +143,7 @@ module Clacky
|
|
|
140
143
|
# Report the error
|
|
141
144
|
say "\n❌ Error: #{e.message}", :red
|
|
142
145
|
say e.backtrace.first(5).join("\n"), :red if options[:verbose]
|
|
143
|
-
|
|
146
|
+
|
|
144
147
|
# Show session saved message
|
|
145
148
|
if session_manager&.last_saved_path
|
|
146
149
|
say "\n📂 Session saved: #{session_manager.last_saved_path}", :yellow
|
|
@@ -149,7 +152,7 @@ module Clacky
|
|
|
149
152
|
# Guide user to recover
|
|
150
153
|
say "\n💡 To recover and retry, run:", :yellow
|
|
151
154
|
say " clacky agent -c", :cyan
|
|
152
|
-
|
|
155
|
+
|
|
153
156
|
exit 1
|
|
154
157
|
ensure
|
|
155
158
|
Dir.chdir(original_dir)
|
|
@@ -214,12 +217,14 @@ module Clacky
|
|
|
214
217
|
end
|
|
215
218
|
|
|
216
219
|
def display_agent_event(event)
|
|
220
|
+
formatter = ui_formatter
|
|
221
|
+
|
|
217
222
|
case event[:type]
|
|
218
223
|
when :thinking
|
|
219
|
-
|
|
224
|
+
formatter.thinking
|
|
220
225
|
when :assistant_message
|
|
221
226
|
# Display assistant's thinking/explanation before tool calls
|
|
222
|
-
|
|
227
|
+
formatter.assistant_message(event[:data][:content])
|
|
223
228
|
when :tool_call
|
|
224
229
|
display_tool_call(event[:data])
|
|
225
230
|
when :observation
|
|
@@ -227,15 +232,15 @@ module Clacky
|
|
|
227
232
|
# Auto-display TODO status if exists
|
|
228
233
|
display_todo_status_if_exists
|
|
229
234
|
when :answer
|
|
230
|
-
|
|
235
|
+
formatter.assistant_message(event[:data][:content])
|
|
231
236
|
when :tool_denied
|
|
232
|
-
|
|
237
|
+
formatter.tool_denied(event[:data][:name])
|
|
233
238
|
when :tool_planned
|
|
234
|
-
|
|
239
|
+
formatter.tool_planned(event[:data][:name])
|
|
235
240
|
when :tool_error
|
|
236
|
-
|
|
241
|
+
formatter.tool_error(event[:data][:error].message)
|
|
237
242
|
when :on_iteration
|
|
238
|
-
|
|
243
|
+
formatter.iteration(event[:data][:iteration]) if options[:verbose]
|
|
239
244
|
end
|
|
240
245
|
end
|
|
241
246
|
|
|
@@ -249,14 +254,14 @@ module Clacky
|
|
|
249
254
|
begin
|
|
250
255
|
args = JSON.parse(args_json, symbolize_names: true)
|
|
251
256
|
formatted = tool.format_call(args)
|
|
252
|
-
|
|
257
|
+
ui_formatter.tool_call(formatted)
|
|
253
258
|
rescue JSON::ParserError, StandardError => e
|
|
254
259
|
say "⚠️ Warning: Failed to format tool call: #{e.message}", :yellow
|
|
255
|
-
|
|
260
|
+
ui_formatter.tool_call("#{tool_name}(...)")
|
|
256
261
|
end
|
|
257
262
|
else
|
|
258
263
|
say "⚠️ Warning: Tool instance not found for '#{tool_name}'", :yellow
|
|
259
|
-
|
|
264
|
+
ui_formatter.tool_call("#{tool_name}(...)")
|
|
260
265
|
end
|
|
261
266
|
|
|
262
267
|
# Show verbose details if requested
|
|
@@ -274,15 +279,15 @@ module Clacky
|
|
|
274
279
|
if tool
|
|
275
280
|
begin
|
|
276
281
|
summary = tool.format_result(result)
|
|
277
|
-
|
|
282
|
+
ui_formatter.tool_result(summary)
|
|
278
283
|
rescue StandardError => e
|
|
279
|
-
|
|
284
|
+
ui_formatter.tool_result("Done")
|
|
280
285
|
end
|
|
281
286
|
else
|
|
282
287
|
# Fallback for unknown tools
|
|
283
288
|
result_str = result.to_s
|
|
284
289
|
summary = result_str.length > 100 ? "#{result_str[0..100]}..." : result_str
|
|
285
|
-
|
|
290
|
+
ui_formatter.tool_result(summary)
|
|
286
291
|
end
|
|
287
292
|
|
|
288
293
|
# Show verbose details if requested
|
|
@@ -313,42 +318,7 @@ module Clacky
|
|
|
313
318
|
todos = @current_agent.todos
|
|
314
319
|
return if todos.empty?
|
|
315
320
|
|
|
316
|
-
|
|
317
|
-
completed = todos.count { |t| t[:status] == "completed" }
|
|
318
|
-
total = todos.size
|
|
319
|
-
|
|
320
|
-
# Build progress bar
|
|
321
|
-
progress_bar = todos.map { |t| t[:status] == "completed" ? "✓" : "○" }.join
|
|
322
|
-
|
|
323
|
-
# Check if all completed
|
|
324
|
-
if completed == total
|
|
325
|
-
say "\n📋 Tasks [#{completed}/#{total}]: #{progress_bar} 🎉 All completed!", :green
|
|
326
|
-
return
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
# Find current and next tasks
|
|
330
|
-
current_task = todos.find { |t| t[:status] == "pending" }
|
|
331
|
-
next_task_index = todos.index(current_task)
|
|
332
|
-
next_task = next_task_index && todos[next_task_index + 1]
|
|
333
|
-
|
|
334
|
-
say "\n📋 Tasks [#{completed}/#{total}]: #{progress_bar}", :yellow
|
|
335
|
-
if current_task
|
|
336
|
-
say " → Next: ##{current_task[:id]} - #{current_task[:task]}", :white
|
|
337
|
-
end
|
|
338
|
-
if next_task && next_task[:status] == "pending"
|
|
339
|
-
say " ⇢ After that: ##{next_task[:id]} - #{next_task[:task]}", :white
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def display_agent_result(result)
|
|
344
|
-
say "\n" + ("=" * 60), :cyan
|
|
345
|
-
say "Agent Session Complete", :green
|
|
346
|
-
say "=" * 60, :cyan
|
|
347
|
-
say "Status: #{result[:status]}", :green
|
|
348
|
-
say "Iterations: #{result[:iterations]}", :yellow
|
|
349
|
-
say "Duration: #{result[:duration_seconds].round(2)}s", :yellow
|
|
350
|
-
say "Total Cost: $#{result[:total_cost_usd]}", :yellow
|
|
351
|
-
say "=" * 60, :cyan
|
|
321
|
+
ui_formatter.todo_status(todos)
|
|
352
322
|
end
|
|
353
323
|
|
|
354
324
|
def validate_working_directory(path)
|
|
@@ -387,26 +357,37 @@ module Clacky
|
|
|
387
357
|
# Store agent as instance variable for access in display methods
|
|
388
358
|
@current_agent = agent
|
|
389
359
|
|
|
360
|
+
# Initialize UI components
|
|
361
|
+
banner = ui_banner
|
|
362
|
+
prompt = ui_prompt
|
|
363
|
+
statusbar = ui_statusbar
|
|
364
|
+
|
|
365
|
+
# Show startup banner for new session
|
|
366
|
+
if agent.total_tasks == 0
|
|
367
|
+
banner.display_startup
|
|
368
|
+
end
|
|
369
|
+
|
|
390
370
|
# Show session info if continuing
|
|
391
371
|
if agent.total_tasks > 0
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
372
|
+
banner.display_session_continue(
|
|
373
|
+
session_id: agent.session_id[0..7],
|
|
374
|
+
created_at: Time.parse(agent.created_at).strftime('%Y-%m-%d %H:%M'),
|
|
375
|
+
tasks: agent.total_tasks,
|
|
376
|
+
cost: agent.total_cost.round(4)
|
|
377
|
+
)
|
|
397
378
|
|
|
398
379
|
# Show recent conversation history
|
|
399
380
|
display_recent_messages(agent.messages, limit: 5)
|
|
381
|
+
else
|
|
382
|
+
# Show welcome info for new session
|
|
383
|
+
banner.display_agent_welcome(
|
|
384
|
+
working_dir: working_dir,
|
|
385
|
+
mode: agent_config.permission_mode,
|
|
386
|
+
max_iterations: agent_config.max_iterations,
|
|
387
|
+
max_cost: agent_config.max_cost_usd
|
|
388
|
+
)
|
|
400
389
|
end
|
|
401
390
|
|
|
402
|
-
say "🤖 Starting interactive agent mode...", :green
|
|
403
|
-
say "Working directory: #{working_dir}", :cyan
|
|
404
|
-
say "Mode: #{agent_config.permission_mode}", :yellow
|
|
405
|
-
say "Max iterations: #{agent_config.max_iterations} per task", :yellow
|
|
406
|
-
say "Max cost: $#{agent_config.max_cost_usd} per task", :yellow
|
|
407
|
-
say "\nType 'exit' or 'quit' to end the session.\n", :yellow
|
|
408
|
-
|
|
409
|
-
prompt = TTY::Prompt.new
|
|
410
391
|
total_tasks = agent.total_tasks
|
|
411
392
|
total_cost = agent.total_cost
|
|
412
393
|
|
|
@@ -418,15 +399,26 @@ module Clacky
|
|
|
418
399
|
unless current_message && !current_message.strip.empty?
|
|
419
400
|
say "\n" if total_tasks > 0
|
|
420
401
|
|
|
421
|
-
#
|
|
422
|
-
|
|
402
|
+
# Show status bar before input
|
|
403
|
+
statusbar.display(
|
|
404
|
+
working_dir: working_dir,
|
|
405
|
+
mode: agent_config.permission_mode.to_s,
|
|
406
|
+
model: agent_config.model,
|
|
407
|
+
tasks: total_tasks,
|
|
408
|
+
cost: total_cost
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Use enhanced prompt with "You:" prefix
|
|
412
|
+
current_message = prompt.read_input(prefix: "You:")
|
|
423
413
|
|
|
424
414
|
break if current_message.nil? || %w[exit quit].include?(current_message&.downcase&.strip)
|
|
425
415
|
next if current_message.strip.empty?
|
|
416
|
+
|
|
417
|
+
# Display user's message after input
|
|
418
|
+
ui_formatter.user_message(current_message)
|
|
426
419
|
end
|
|
427
420
|
|
|
428
421
|
total_tasks += 1
|
|
429
|
-
say "\n"
|
|
430
422
|
|
|
431
423
|
begin
|
|
432
424
|
result = agent.run(current_message) do |event|
|
|
@@ -441,17 +433,18 @@ module Clacky
|
|
|
441
433
|
end
|
|
442
434
|
|
|
443
435
|
# Show brief task completion
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
436
|
+
banner.display_task_complete(
|
|
437
|
+
iterations: result[:iterations],
|
|
438
|
+
cost: result[:total_cost_usd].round(4),
|
|
439
|
+
total_tasks: total_tasks,
|
|
440
|
+
total_cost: total_cost.round(4),
|
|
441
|
+
cache_stats: result[:cache_stats]
|
|
442
|
+
)
|
|
450
443
|
rescue Clacky::AgentInterrupted
|
|
451
444
|
# Save session on interruption
|
|
452
445
|
if session_manager
|
|
453
446
|
session_manager.save(agent.to_session_data(status: :interrupted))
|
|
454
|
-
|
|
447
|
+
ui_formatter.warning("Task interrupted by user (Ctrl+C)")
|
|
455
448
|
say "You can start a new task or type 'exit' to quit.\n", :yellow
|
|
456
449
|
end
|
|
457
450
|
rescue StandardError => e
|
|
@@ -461,17 +454,15 @@ module Clacky
|
|
|
461
454
|
end
|
|
462
455
|
|
|
463
456
|
# Report the error
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
457
|
+
banner.display_error(e.message, details: options[:verbose] ? e.backtrace.first(3).join("\n") : nil)
|
|
458
|
+
|
|
467
459
|
# Show session saved message
|
|
468
460
|
if session_manager&.last_saved_path
|
|
469
|
-
|
|
461
|
+
ui_formatter.info("Session saved: #{session_manager.last_saved_path}")
|
|
470
462
|
end
|
|
471
463
|
|
|
472
464
|
# Guide user to recover
|
|
473
|
-
|
|
474
|
-
say " clacky agent -c", :cyan
|
|
465
|
+
ui_formatter.info("To recover and retry, run: clacky agent -c")
|
|
475
466
|
say "\nOr you can continue with a new task or type 'exit' to quit.", :yellow
|
|
476
467
|
end
|
|
477
468
|
|
|
@@ -484,9 +475,10 @@ module Clacky
|
|
|
484
475
|
session_manager.save(agent.to_session_data)
|
|
485
476
|
end
|
|
486
477
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
478
|
+
banner.display_goodbye(
|
|
479
|
+
total_tasks: total_tasks,
|
|
480
|
+
total_cost: total_cost.round(4)
|
|
481
|
+
)
|
|
490
482
|
end
|
|
491
483
|
|
|
492
484
|
def list_sessions
|
|
@@ -582,21 +574,23 @@ module Clacky
|
|
|
582
574
|
return
|
|
583
575
|
end
|
|
584
576
|
|
|
585
|
-
|
|
586
|
-
|
|
577
|
+
formatter = ui_formatter
|
|
578
|
+
formatter.separator("─")
|
|
579
|
+
say pastel.dim("Recent conversation history:"), :yellow
|
|
580
|
+
formatter.separator("─")
|
|
587
581
|
|
|
588
582
|
recent.each do |msg|
|
|
589
583
|
case msg[:role]
|
|
590
584
|
when "user"
|
|
591
585
|
content = truncate_message(msg[:content], 150)
|
|
592
|
-
say "
|
|
586
|
+
say " #{pastel.blue('[>>]')} You: #{content}"
|
|
593
587
|
when "assistant"
|
|
594
588
|
content = truncate_message(msg[:content], 200)
|
|
595
|
-
say "
|
|
589
|
+
say " #{pastel.green('[<<]')} Assistant: #{content}"
|
|
596
590
|
end
|
|
597
591
|
end
|
|
598
592
|
|
|
599
|
-
|
|
593
|
+
formatter.separator("─")
|
|
600
594
|
say ""
|
|
601
595
|
end
|
|
602
596
|
|
|
@@ -612,6 +606,27 @@ module Clacky
|
|
|
612
606
|
cleaned
|
|
613
607
|
end
|
|
614
608
|
end
|
|
609
|
+
|
|
610
|
+
# UI component accessors
|
|
611
|
+
def ui_banner
|
|
612
|
+
@ui_banner ||= UI::Banner.new
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def ui_prompt
|
|
616
|
+
@ui_prompt ||= UI::Prompt.new
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def ui_statusbar
|
|
620
|
+
@ui_statusbar ||= UI::StatusBar.new
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def ui_formatter
|
|
624
|
+
@ui_formatter ||= UI::Formatter.new
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def pastel
|
|
628
|
+
@pastel ||= Pastel.new
|
|
629
|
+
end
|
|
615
630
|
end
|
|
616
631
|
|
|
617
632
|
private
|
|
@@ -641,12 +656,23 @@ module Clacky
|
|
|
641
656
|
base_url: config.base_url
|
|
642
657
|
)
|
|
643
658
|
|
|
659
|
+
# Use TTY::Prompt for input
|
|
660
|
+
tty_prompt = TTY::Prompt.new(interrupt: :exit)
|
|
661
|
+
|
|
644
662
|
loop do
|
|
645
|
-
# Use
|
|
646
|
-
|
|
663
|
+
# Use TTY::Prompt for better input handling
|
|
664
|
+
begin
|
|
665
|
+
message = tty_prompt.ask("You:", required: false) do |q|
|
|
666
|
+
q.modify :strip
|
|
667
|
+
end
|
|
668
|
+
rescue TTY::Reader::InputInterrupt
|
|
669
|
+
# Handle Ctrl+C
|
|
670
|
+
puts
|
|
671
|
+
break
|
|
672
|
+
end
|
|
647
673
|
|
|
648
|
-
break if message.nil? || %w[exit quit].include?(message
|
|
649
|
-
next if message.strip.empty?
|
|
674
|
+
break if message.nil? || %w[exit quit].include?(message&.downcase&.strip)
|
|
675
|
+
next if message.nil? || message.strip.empty?
|
|
650
676
|
|
|
651
677
|
spinner = TTY::Spinner.new("[:spinner] Claude is thinking...", format: :dots)
|
|
652
678
|
spinner.auto_spin
|
data/lib/clacky/client.rb
CHANGED
|
@@ -43,7 +43,9 @@ module Clacky
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Send messages with function calling (tools) support
|
|
46
|
-
|
|
46
|
+
# Options:
|
|
47
|
+
# - enable_caching: Enable prompt caching for system prompt and tools (default: false)
|
|
48
|
+
def send_messages_with_tools(messages, model:, tools:, max_tokens:, verbose: false, enable_caching: false)
|
|
47
49
|
body = {
|
|
48
50
|
model: model,
|
|
49
51
|
max_tokens: max_tokens,
|
|
@@ -51,7 +53,18 @@ module Clacky
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
# Add tools if provided
|
|
54
|
-
|
|
56
|
+
# For Claude API with caching: mark the last tool definition with cache_control
|
|
57
|
+
if tools&.any?
|
|
58
|
+
if enable_caching && supports_prompt_caching?(model)
|
|
59
|
+
# Deep clone tools to avoid modifying original
|
|
60
|
+
cached_tools = tools.map { |tool| deep_clone(tool) }
|
|
61
|
+
# Mark the last tool for caching (Claude caches from cache breakpoint to end)
|
|
62
|
+
cached_tools.last[:cache_control] = { type: "ephemeral" }
|
|
63
|
+
body[:tools] = cached_tools
|
|
64
|
+
else
|
|
65
|
+
body[:tools] = tools
|
|
66
|
+
end
|
|
67
|
+
end
|
|
55
68
|
|
|
56
69
|
# Debug output
|
|
57
70
|
if verbose || ENV["CLACKY_DEBUG"]
|
|
@@ -77,6 +90,31 @@ module Clacky
|
|
|
77
90
|
|
|
78
91
|
private
|
|
79
92
|
|
|
93
|
+
# Check if the model supports prompt caching
|
|
94
|
+
# Currently only Claude 3.5 Sonnet and newer Claude models support this
|
|
95
|
+
def supports_prompt_caching?(model)
|
|
96
|
+
model_str = model.to_s.downcase
|
|
97
|
+
# Claude 3.5 Sonnet (20241022 and newer) supports prompt caching
|
|
98
|
+
# Also Claude 3.7 Sonnet and Opus models when they're released
|
|
99
|
+
model_str.include?("claude-3.5-sonnet") ||
|
|
100
|
+
model_str.include?("claude-3-7") ||
|
|
101
|
+
model_str.include?("claude-4")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Deep clone a hash/array structure (for tool definitions)
|
|
105
|
+
def deep_clone(obj)
|
|
106
|
+
case obj
|
|
107
|
+
when Hash
|
|
108
|
+
obj.each_with_object({}) { |(k, v), h| h[k] = deep_clone(v) }
|
|
109
|
+
when Array
|
|
110
|
+
obj.map { |item| deep_clone(item) }
|
|
111
|
+
when String, Symbol, Integer, Float, TrueClass, FalseClass, NilClass
|
|
112
|
+
obj
|
|
113
|
+
else
|
|
114
|
+
obj.dup rescue obj
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
80
118
|
def connection
|
|
81
119
|
@connection ||= Faraday.new(url: @base_url) do |conn|
|
|
82
120
|
conn.headers["Content-Type"] = "application/json"
|
|
@@ -117,15 +155,26 @@ module Clacky
|
|
|
117
155
|
puts " content length: #{message["content"]&.length || 0}"
|
|
118
156
|
end
|
|
119
157
|
|
|
158
|
+
# Parse usage with cache information
|
|
159
|
+
usage_data = {
|
|
160
|
+
prompt_tokens: usage["prompt_tokens"],
|
|
161
|
+
completion_tokens: usage["completion_tokens"],
|
|
162
|
+
total_tokens: usage["total_tokens"]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Add cache metrics if present (Claude API with prompt caching)
|
|
166
|
+
if usage["cache_creation_input_tokens"]
|
|
167
|
+
usage_data[:cache_creation_input_tokens] = usage["cache_creation_input_tokens"]
|
|
168
|
+
end
|
|
169
|
+
if usage["cache_read_input_tokens"]
|
|
170
|
+
usage_data[:cache_read_input_tokens] = usage["cache_read_input_tokens"]
|
|
171
|
+
end
|
|
172
|
+
|
|
120
173
|
{
|
|
121
174
|
content: message["content"],
|
|
122
175
|
tool_calls: parse_tool_calls(message["tool_calls"]),
|
|
123
176
|
finish_reason: data["choices"].first["finish_reason"],
|
|
124
|
-
usage:
|
|
125
|
-
prompt_tokens: usage["prompt_tokens"],
|
|
126
|
-
completion_tokens: usage["completion_tokens"],
|
|
127
|
-
total_tokens: usage["total_tokens"]
|
|
128
|
-
}
|
|
177
|
+
usage: usage_data
|
|
129
178
|
}
|
|
130
179
|
when 401
|
|
131
180
|
raise Error, "Invalid API key"
|
data/lib/clacky/hook_manager.rb
CHANGED
|
@@ -14,7 +14,9 @@ module Clacky
|
|
|
14
14
|
def start
|
|
15
15
|
@start_time = Time.now
|
|
16
16
|
@running = true
|
|
17
|
-
|
|
17
|
+
# Save cursor position after the [..] symbol
|
|
18
|
+
print "\e[s" # Save cursor position
|
|
19
|
+
print_thinking_status("#{@thinking_verb}… (ctrl+c to interrupt)")
|
|
18
20
|
|
|
19
21
|
# Start background thread to update elapsed time
|
|
20
22
|
@update_thread = Thread.new do
|
|
@@ -29,24 +31,25 @@ module Clacky
|
|
|
29
31
|
return unless @start_time
|
|
30
32
|
|
|
31
33
|
elapsed = (Time.now - @start_time).to_i
|
|
32
|
-
|
|
34
|
+
print_thinking_status("#{@thinking_verb}… (ctrl+c to interrupt · #{elapsed}s)")
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
def finish
|
|
36
38
|
@running = false
|
|
37
39
|
@update_thread&.join
|
|
38
|
-
|
|
40
|
+
# Restore cursor and clear to end of line
|
|
41
|
+
print "\e[u" # Restore cursor position
|
|
42
|
+
print "\e[K" # Clear to end of line
|
|
43
|
+
puts "" # Add newline after finishing
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
private
|
|
42
47
|
|
|
43
|
-
def
|
|
44
|
-
print "\
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def clear_line
|
|
49
|
-
print "\r\033[K" # Clear the entire line
|
|
48
|
+
def print_thinking_status(text)
|
|
49
|
+
print "\e[u" # Restore cursor position (to after [..] symbol)
|
|
50
|
+
print "\e[K" # Clear to end of line from cursor
|
|
51
|
+
print text
|
|
52
|
+
print " "
|
|
50
53
|
$stdout.flush
|
|
51
54
|
end
|
|
52
55
|
end
|
|
@@ -147,6 +147,12 @@ module Clacky
|
|
|
147
147
|
trash_directory = Clacky::TrashDirectory.new(@project_root)
|
|
148
148
|
@trash_dir = trash_directory.trash_dir
|
|
149
149
|
@backup_dir = trash_directory.backup_dir
|
|
150
|
+
|
|
151
|
+
# Setup safety log directory under ~/.clacky/safety_logs/
|
|
152
|
+
@project_hash = trash_directory.generate_project_hash(@project_root)
|
|
153
|
+
@safety_log_dir = File.join(Dir.home, ".clacky", "safety_logs", @project_hash)
|
|
154
|
+
FileUtils.mkdir_p(@safety_log_dir) unless Dir.exist?(@safety_log_dir)
|
|
155
|
+
@safety_log_file = File.join(@safety_log_dir, "safety.log")
|
|
150
156
|
end
|
|
151
157
|
|
|
152
158
|
def make_command_safe(command)
|
|
@@ -384,8 +390,7 @@ module Clacky
|
|
|
384
390
|
end
|
|
385
391
|
|
|
386
392
|
def write_log(log_entry)
|
|
387
|
-
|
|
388
|
-
File.open(log_file, 'a') do |f|
|
|
393
|
+
File.open(@safety_log_file, 'a') do |f|
|
|
389
394
|
f.puts JSON.generate(log_entry)
|
|
390
395
|
end
|
|
391
396
|
rescue StandardError
|
|
@@ -260,7 +260,7 @@ module Clacky
|
|
|
260
260
|
- Use 'list' to see what files are in trash
|
|
261
261
|
- Use 'restore' to get back accidentally deleted files
|
|
262
262
|
- Use 'empty' periodically to free up disk space
|
|
263
|
-
- All deletions by SafeShell are logged in
|
|
263
|
+
- All deletions by SafeShell are logged in ~/.clacky/safety_logs/
|
|
264
264
|
HELP
|
|
265
265
|
|
|
266
266
|
{
|