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,434 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
class Agent
|
|
5
|
+
# Tool execution and permission management
|
|
6
|
+
# Handles tool confirmation, preview, and result building
|
|
7
|
+
module ToolExecutor
|
|
8
|
+
# Check if a tool should be auto-executed based on permission mode
|
|
9
|
+
# @param tool_name [String] Name of the tool
|
|
10
|
+
# @param tool_params [Hash, String] Tool parameters
|
|
11
|
+
# @return [Boolean] true if should auto-execute
|
|
12
|
+
def should_auto_execute?(tool_name, tool_params = {})
|
|
13
|
+
case @config.permission_mode
|
|
14
|
+
when :auto_approve
|
|
15
|
+
true
|
|
16
|
+
when :confirm_safes
|
|
17
|
+
# Use SafeShell integration for safety check
|
|
18
|
+
is_safe_operation?(tool_name, tool_params)
|
|
19
|
+
when :plan_only
|
|
20
|
+
false
|
|
21
|
+
else
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if an operation is considered safe for auto-execution
|
|
27
|
+
# @param tool_name [String] Name of the tool
|
|
28
|
+
# @param tool_params [Hash, String] Tool parameters
|
|
29
|
+
# @return [Boolean] true if safe operation
|
|
30
|
+
def is_safe_operation?(tool_name, tool_params = {})
|
|
31
|
+
# For shell commands, use SafeShell to check safety
|
|
32
|
+
if tool_name.to_s.downcase == 'shell' || tool_name.to_s.downcase == 'safe_shell'
|
|
33
|
+
params = tool_params.is_a?(String) ? JSON.parse(tool_params) : tool_params
|
|
34
|
+
command = params[:command] || params['command']
|
|
35
|
+
return false unless command
|
|
36
|
+
|
|
37
|
+
return Tools::SafeShell.command_safe_for_auto_execution?(command)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if tool_name.to_s.downcase == 'edit' || tool_name.to_s.downcase == 'write'
|
|
41
|
+
return false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Request user confirmation for tool execution
|
|
48
|
+
# Shows preview and returns approval status
|
|
49
|
+
# @param call [Hash] Tool call with :name and :arguments
|
|
50
|
+
# @return [Hash] { approved: Boolean, feedback: String, system_injected: Boolean }
|
|
51
|
+
def confirm_tool_use?(call)
|
|
52
|
+
# Show preview first and check for errors
|
|
53
|
+
preview_error = show_tool_preview(call)
|
|
54
|
+
|
|
55
|
+
# If preview detected an error, auto-deny and provide feedback
|
|
56
|
+
if preview_error && preview_error[:error]
|
|
57
|
+
feedback = build_preview_error_feedback(call[:name], preview_error)
|
|
58
|
+
return { approved: false, feedback: feedback, system_injected: true }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Request confirmation via UI
|
|
62
|
+
if @ui
|
|
63
|
+
prompt_text = format_tool_prompt(call)
|
|
64
|
+
result = @ui.request_confirmation(prompt_text, default: true)
|
|
65
|
+
|
|
66
|
+
case result
|
|
67
|
+
when true
|
|
68
|
+
{ approved: true, feedback: nil }
|
|
69
|
+
when false, nil
|
|
70
|
+
# User denied - add visual marker based on tool type
|
|
71
|
+
tool_name_capitalized = call[:name].capitalize
|
|
72
|
+
@ui&.show_info(" ↳ #{tool_name_capitalized} cancelled", prefix_newline: false)
|
|
73
|
+
{ approved: false, feedback: nil }
|
|
74
|
+
else
|
|
75
|
+
# String feedback - also add visual marker
|
|
76
|
+
tool_name_capitalized = call[:name].capitalize
|
|
77
|
+
@ui&.show_info(" ↳ #{tool_name_capitalized} cancelled", prefix_newline: false)
|
|
78
|
+
{ approved: false, feedback: result.to_s }
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
# Fallback: auto-approve if no UI
|
|
82
|
+
{ approved: true, feedback: nil }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Show preview for tool execution
|
|
87
|
+
# @param call [Hash] Tool call with :name and :arguments
|
|
88
|
+
# @return [Hash, nil] Error information if preview detected issues
|
|
89
|
+
def show_tool_preview(call)
|
|
90
|
+
return nil unless @ui
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
args = JSON.parse(call[:arguments], symbolize_names: true)
|
|
94
|
+
|
|
95
|
+
preview_error = nil
|
|
96
|
+
case call[:name]
|
|
97
|
+
when "write"
|
|
98
|
+
preview_error = show_write_preview(args)
|
|
99
|
+
when "edit"
|
|
100
|
+
preview_error = show_edit_preview(args)
|
|
101
|
+
when "shell", "safe_shell"
|
|
102
|
+
show_shell_preview(args)
|
|
103
|
+
else
|
|
104
|
+
# For other tools, show formatted arguments
|
|
105
|
+
tool = @tool_registry.get(call[:name]) rescue nil
|
|
106
|
+
if tool
|
|
107
|
+
formatted = tool.format_call(args) rescue "#{call[:name]}(...)"
|
|
108
|
+
@ui&.show_tool_args(formatted)
|
|
109
|
+
else
|
|
110
|
+
@ui&.show_tool_args(call[:arguments])
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
preview_error
|
|
115
|
+
rescue JSON::ParserError
|
|
116
|
+
@ui&.show_tool_args(call[:arguments])
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Format tool call for user confirmation prompt
|
|
122
|
+
# @param call [Hash] Tool call with :name and :arguments
|
|
123
|
+
# @return [String] Formatted prompt text
|
|
124
|
+
def format_tool_prompt(call)
|
|
125
|
+
begin
|
|
126
|
+
args = JSON.parse(call[:arguments], symbolize_names: true)
|
|
127
|
+
|
|
128
|
+
# Try to use tool's format_call method for better formatting
|
|
129
|
+
tool = @tool_registry.get(call[:name]) rescue nil
|
|
130
|
+
if tool
|
|
131
|
+
formatted = tool.format_call(args) rescue nil
|
|
132
|
+
return formatted if formatted
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Fallback to manual formatting for common tools
|
|
136
|
+
case call[:name]
|
|
137
|
+
when "edit"
|
|
138
|
+
path = args[:path] || args[:file_path]
|
|
139
|
+
filename = Utils::PathHelper.safe_basename(path)
|
|
140
|
+
"Edit(#{filename})"
|
|
141
|
+
when "write"
|
|
142
|
+
filename = Utils::PathHelper.safe_basename(args[:path])
|
|
143
|
+
if args[:path] && File.exist?(args[:path])
|
|
144
|
+
"Write(#{filename}) - overwrite existing"
|
|
145
|
+
else
|
|
146
|
+
"Write(#{filename}) - create new"
|
|
147
|
+
end
|
|
148
|
+
when "shell", "safe_shell"
|
|
149
|
+
cmd = args[:command] || ''
|
|
150
|
+
display_cmd = cmd.length > 30 ? "#{cmd[0..27]}..." : cmd
|
|
151
|
+
"#{call[:name]}(\"#{display_cmd}\")"
|
|
152
|
+
else
|
|
153
|
+
"Allow #{call[:name]}"
|
|
154
|
+
end
|
|
155
|
+
rescue JSON::ParserError
|
|
156
|
+
"Allow #{call[:name]}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Build success result for tool execution
|
|
161
|
+
# @param call [Hash] Tool call
|
|
162
|
+
# @param result [Object] Tool execution result
|
|
163
|
+
# @return [Hash] Formatted result for LLM
|
|
164
|
+
def build_success_result(call, result)
|
|
165
|
+
# Try to get tool instance to use its format_result_for_llm method
|
|
166
|
+
tool = @tool_registry.get(call[:name]) rescue nil
|
|
167
|
+
|
|
168
|
+
formatted_result = if tool && tool.respond_to?(:format_result_for_llm)
|
|
169
|
+
# Tool provides a custom LLM-friendly format
|
|
170
|
+
tool.format_result_for_llm(result)
|
|
171
|
+
else
|
|
172
|
+
# Fallback: use the original result
|
|
173
|
+
result
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Inject TODO reminder for non-todo_manager tools
|
|
177
|
+
formatted_result = inject_todo_reminder(call[:name], formatted_result)
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
id: call[:id],
|
|
181
|
+
content: JSON.generate(formatted_result)
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Build error result for tool execution
|
|
186
|
+
# @param call [Hash] Tool call
|
|
187
|
+
# @param error_message [String] Error message
|
|
188
|
+
# @return [Hash] Formatted error result
|
|
189
|
+
def build_error_result(call, error_message)
|
|
190
|
+
{
|
|
191
|
+
id: call[:id],
|
|
192
|
+
content: JSON.generate({ error: error_message })
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Build denied result when user denies tool execution
|
|
197
|
+
# @param call [Hash] Tool call
|
|
198
|
+
# @param user_feedback [String, nil] User's feedback message
|
|
199
|
+
# @param system_injected [Boolean] Whether this is a system-generated denial
|
|
200
|
+
# @return [Hash] Formatted denial result
|
|
201
|
+
def build_denied_result(call, user_feedback = nil, system_injected = false)
|
|
202
|
+
if system_injected
|
|
203
|
+
# System-generated feedback (e.g., from preview errors)
|
|
204
|
+
tool_content = {
|
|
205
|
+
error: "Tool #{call[:name]} denied: #{user_feedback}",
|
|
206
|
+
system_injected: true
|
|
207
|
+
}
|
|
208
|
+
else
|
|
209
|
+
# User manually denied or provided feedback
|
|
210
|
+
message = if user_feedback && !user_feedback.empty?
|
|
211
|
+
"Tool use denied by user. User feedback: #{user_feedback}"
|
|
212
|
+
else
|
|
213
|
+
"Tool use denied by user"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
tool_content = {
|
|
217
|
+
error: message,
|
|
218
|
+
user_feedback: user_feedback
|
|
219
|
+
}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
{
|
|
223
|
+
id: call[:id],
|
|
224
|
+
content: JSON.generate(tool_content)
|
|
225
|
+
}
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Build planned result for plan-only mode
|
|
229
|
+
# @param call [Hash] Tool call
|
|
230
|
+
# @return [Hash] Formatted planned result
|
|
231
|
+
def build_planned_result(call)
|
|
232
|
+
{
|
|
233
|
+
id: call[:id],
|
|
234
|
+
content: JSON.generate({ planned: true, message: "Tool execution skipped (plan mode)" })
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Check if a tool is potentially slow and should show progress
|
|
239
|
+
# @param tool_name [String] Name of the tool
|
|
240
|
+
# @param args [Hash] Tool arguments
|
|
241
|
+
# @return [Boolean] true if tool is potentially slow
|
|
242
|
+
private def potentially_slow_tool?(tool_name, args)
|
|
243
|
+
case tool_name.to_s.downcase
|
|
244
|
+
when 'shell', 'safe_shell'
|
|
245
|
+
# Check if the command is a slow command
|
|
246
|
+
command = args[:command] || args['command']
|
|
247
|
+
return false unless command
|
|
248
|
+
|
|
249
|
+
# List of slow command patterns
|
|
250
|
+
slow_patterns = [
|
|
251
|
+
/bundle\s+(install|exec\s+rspec|exec\s+rake)/,
|
|
252
|
+
/npm\s+(install|run\s+test|run\s+build)/,
|
|
253
|
+
/yarn\s+(install|test|build)/,
|
|
254
|
+
/pnpm\s+install/,
|
|
255
|
+
/cargo\s+(build|test)/,
|
|
256
|
+
/go\s+(build|test)/,
|
|
257
|
+
/make\s+(test|build)/,
|
|
258
|
+
/pytest/,
|
|
259
|
+
/jest/
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
slow_patterns.any? { |pattern| command.match?(pattern) }
|
|
263
|
+
when 'web_fetch', 'web_search'
|
|
264
|
+
true # Network operations can be slow
|
|
265
|
+
else
|
|
266
|
+
false # Most file operations are fast
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Build progress message for tool execution
|
|
271
|
+
# @param tool_name [String] Name of the tool
|
|
272
|
+
# @param args [Hash] Tool arguments
|
|
273
|
+
# @return [String] Progress message
|
|
274
|
+
private def build_tool_progress_message(tool_name, args)
|
|
275
|
+
case tool_name.to_s.downcase
|
|
276
|
+
when 'shell', 'safe_shell'
|
|
277
|
+
"Running command"
|
|
278
|
+
when 'web_fetch'
|
|
279
|
+
"Fetching web page"
|
|
280
|
+
when 'web_search'
|
|
281
|
+
"Searching web"
|
|
282
|
+
else
|
|
283
|
+
"Executing #{tool_name}"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Inject TODO reminder into tool results for non-todo_manager tools
|
|
288
|
+
# This helps AI remember to mark TODOs as complete after executing tasks
|
|
289
|
+
# @param tool_name [String] Name of the tool
|
|
290
|
+
# @param result [Object] Tool execution result
|
|
291
|
+
# @return [Object] Result with optional TODO reminder
|
|
292
|
+
private def inject_todo_reminder(tool_name, result)
|
|
293
|
+
# Skip injection for todo_manager tool itself to avoid redundancy
|
|
294
|
+
return result if tool_name == "todo_manager"
|
|
295
|
+
|
|
296
|
+
# Get pending TODOs
|
|
297
|
+
todo_tool = @tool_registry.get("todo_manager")
|
|
298
|
+
return result unless todo_tool
|
|
299
|
+
|
|
300
|
+
pending_todos = begin
|
|
301
|
+
todo_result = todo_tool.execute(action: "list", todos_storage: @todos)
|
|
302
|
+
if todo_result.is_a?(Hash) && todo_result[:todos]
|
|
303
|
+
todo_result[:todos].select { |t| t[:status] == "pending" }
|
|
304
|
+
else
|
|
305
|
+
[]
|
|
306
|
+
end
|
|
307
|
+
rescue
|
|
308
|
+
[]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Only inject reminder if there are pending TODOs
|
|
312
|
+
return result unless pending_todos && !pending_todos.empty?
|
|
313
|
+
|
|
314
|
+
# Create a friendly reminder message
|
|
315
|
+
reminder = "\n\n📋 REMINDER: You have #{pending_todos.length} pending TODO(s). " \
|
|
316
|
+
"After completing each task, remember to mark it as complete using " \
|
|
317
|
+
"todo_manager with action 'complete' and the task id."
|
|
318
|
+
|
|
319
|
+
# Inject reminder based on result type
|
|
320
|
+
case result
|
|
321
|
+
when String
|
|
322
|
+
result + reminder
|
|
323
|
+
when Hash
|
|
324
|
+
result.merge({ _todo_reminder: reminder.strip })
|
|
325
|
+
when Array
|
|
326
|
+
result + [{ _todo_reminder: reminder.strip }]
|
|
327
|
+
else
|
|
328
|
+
result
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Build feedback message from preview error
|
|
333
|
+
# @param tool_name [String] Name of the tool
|
|
334
|
+
# @param error_info [Hash] Error information from preview
|
|
335
|
+
# @return [String] Feedback message
|
|
336
|
+
private def build_preview_error_feedback(tool_name, error_info)
|
|
337
|
+
case tool_name
|
|
338
|
+
when "edit"
|
|
339
|
+
"Tool edit denied: The edit operation will fail because the old_string was not found in the file. " \
|
|
340
|
+
"Please use file_reader to read '#{error_info[:path]}' first, " \
|
|
341
|
+
"find the correct string to replace, and try again with the exact string (including whitespace)."
|
|
342
|
+
else
|
|
343
|
+
"Tool preview error: #{error_info[:error]}"
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Show preview for write tool
|
|
348
|
+
# @param args [Hash] Write tool arguments
|
|
349
|
+
# @return [nil] Always returns nil (no errors for write)
|
|
350
|
+
private def show_write_preview(args)
|
|
351
|
+
path = args[:path] || args['path']
|
|
352
|
+
new_content = args[:content] || args['content'] || ""
|
|
353
|
+
|
|
354
|
+
is_new_file = !(path && File.exist?(path))
|
|
355
|
+
@ui&.show_file_write_preview(path, is_new_file: is_new_file)
|
|
356
|
+
|
|
357
|
+
if is_new_file
|
|
358
|
+
@ui&.show_diff("", new_content, max_lines: 50)
|
|
359
|
+
else
|
|
360
|
+
old_content = File.read(path)
|
|
361
|
+
@ui&.show_diff(old_content, new_content, max_lines: 50)
|
|
362
|
+
end
|
|
363
|
+
nil
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Show preview for edit tool
|
|
367
|
+
# @param args [Hash] Edit tool arguments
|
|
368
|
+
# @return [Hash, nil] Error information if preview detected issues
|
|
369
|
+
private def show_edit_preview(args)
|
|
370
|
+
path = args[:path] || args[:file_path] || args['path'] || args['file_path']
|
|
371
|
+
old_string = args[:old_string] || args['old_string'] || ""
|
|
372
|
+
new_string = args[:new_string] || args['new_string'] || ""
|
|
373
|
+
replace_all = args[:replace_all] || args['replace_all'] || false
|
|
374
|
+
|
|
375
|
+
@ui&.show_file_edit_preview(path)
|
|
376
|
+
|
|
377
|
+
if !path || path.empty?
|
|
378
|
+
@ui&.show_file_error("No file path provided")
|
|
379
|
+
return { error: "No file path provided for edit operation" }
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
unless File.exist?(path)
|
|
383
|
+
@ui&.show_file_error("File not found: #{path}")
|
|
384
|
+
return { error: "File not found: #{path}", path: path }
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
if old_string.empty?
|
|
388
|
+
@ui&.show_file_error("No old_string provided (nothing to replace)")
|
|
389
|
+
return { error: "No old_string provided (nothing to replace)" }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
file_content = File.read(path)
|
|
393
|
+
|
|
394
|
+
# Check if old_string exists in file
|
|
395
|
+
unless file_content.include?(old_string)
|
|
396
|
+
# Log debug info for troubleshooting
|
|
397
|
+
@debug_logs << {
|
|
398
|
+
timestamp: Time.now.iso8601,
|
|
399
|
+
event: "edit_preview_failed",
|
|
400
|
+
path: path,
|
|
401
|
+
looking_for: old_string[0..500],
|
|
402
|
+
file_content_preview: file_content[0..1000],
|
|
403
|
+
file_size: file_content.length
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@ui&.show_file_error("Edit file error")
|
|
407
|
+
return {
|
|
408
|
+
error: "String to replace not found in file",
|
|
409
|
+
path: path,
|
|
410
|
+
looking_for: old_string[0..200]
|
|
411
|
+
}
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Use the same replace logic as the actual tool execution
|
|
415
|
+
new_content = if replace_all
|
|
416
|
+
file_content.gsub(old_string, new_string)
|
|
417
|
+
else
|
|
418
|
+
file_content.sub(old_string, new_string)
|
|
419
|
+
end
|
|
420
|
+
@ui&.show_diff(file_content, new_content, max_lines: 50)
|
|
421
|
+
nil # No error
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Show preview for shell tool
|
|
425
|
+
# @param args [Hash] Shell tool arguments
|
|
426
|
+
# @return [nil] Always returns nil
|
|
427
|
+
private def show_shell_preview(args)
|
|
428
|
+
command = args[:command] || ""
|
|
429
|
+
@ui&.show_shell_preview(command)
|
|
430
|
+
nil
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
@@ -14,7 +14,7 @@ module Clacky
|
|
|
14
14
|
# Handle shell alias to safe_shell for backward compatibility
|
|
15
15
|
name = 'safe_shell' if name == 'shell' && @tools.key?('safe_shell') && !@tools.key?('shell')
|
|
16
16
|
|
|
17
|
-
@tools[name] || raise(
|
|
17
|
+
@tools[name] || raise(Clacky::ToolCallError, "Tool not found: #{name}")
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def all
|