openclacky 0.6.2 → 0.6.4
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/CHANGELOG.md +30 -0
- data/docs/why-openclacky.md +267 -0
- data/lib/clacky/agent.rb +579 -99
- data/lib/clacky/cli.rb +350 -9
- data/lib/clacky/client.rb +519 -58
- data/lib/clacky/config.rb +71 -4
- data/lib/clacky/default_skills/skill-add/SKILL.md +66 -0
- data/lib/clacky/skill.rb +236 -0
- data/lib/clacky/skill_loader.rb +320 -0
- data/lib/clacky/tools/edit.rb +111 -8
- data/lib/clacky/tools/file_reader.rb +112 -9
- data/lib/clacky/tools/glob.rb +9 -2
- data/lib/clacky/tools/grep.rb +9 -14
- data/lib/clacky/tools/safe_shell.rb +14 -8
- data/lib/clacky/tools/shell.rb +89 -52
- data/lib/clacky/tools/web_fetch.rb +81 -18
- data/lib/clacky/ui2/components/command_suggestions.rb +273 -0
- data/lib/clacky/ui2/components/inline_input.rb +34 -15
- data/lib/clacky/ui2/components/input_area.rb +105 -83
- data/lib/clacky/ui2/layout_manager.rb +89 -33
- data/lib/clacky/ui2/line_editor.rb +142 -2
- data/lib/clacky/ui2/themes/hacker_theme.rb +1 -1
- data/lib/clacky/ui2/themes/minimal_theme.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +38 -47
- data/lib/clacky/utils/file_ignore_helper.rb +10 -12
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +4 -1
- metadata +6 -1
data/lib/clacky/tools/grep.rb
CHANGED
|
@@ -59,11 +59,6 @@ module Clacky
|
|
|
59
59
|
description: "Maximum file size in bytes to search (default: 1MB)",
|
|
60
60
|
default: MAX_FILE_SIZE
|
|
61
61
|
},
|
|
62
|
-
max_files_to_search: {
|
|
63
|
-
type: "integer",
|
|
64
|
-
description: "Maximum number of files to search",
|
|
65
|
-
default: 500
|
|
66
|
-
}
|
|
67
62
|
},
|
|
68
63
|
required: %w[pattern]
|
|
69
64
|
}
|
|
@@ -78,7 +73,7 @@ module Clacky
|
|
|
78
73
|
max_matches_per_file: 50,
|
|
79
74
|
max_total_matches: 200,
|
|
80
75
|
max_file_size: MAX_FILE_SIZE,
|
|
81
|
-
max_files_to_search:
|
|
76
|
+
max_files_to_search: 10000
|
|
82
77
|
)
|
|
83
78
|
# Validate pattern
|
|
84
79
|
if pattern.nil? || pattern.strip.empty?
|
|
@@ -135,7 +130,7 @@ module Clacky
|
|
|
135
130
|
end
|
|
136
131
|
|
|
137
132
|
# Skip if file should be ignored (unless it's a config file)
|
|
138
|
-
if Clacky::Utils::FileIgnoreHelper.should_ignore_file?(file, expanded_path, gitignore) &&
|
|
133
|
+
if Clacky::Utils::FileIgnoreHelper.should_ignore_file?(file, expanded_path, gitignore) &&
|
|
139
134
|
!Clacky::Utils::FileIgnoreHelper.is_config_file?(file)
|
|
140
135
|
skipped[:ignored] += 1
|
|
141
136
|
next
|
|
@@ -217,12 +212,12 @@ module Clacky
|
|
|
217
212
|
matches = result[:total_matches] || 0
|
|
218
213
|
files = result[:files_with_matches] || 0
|
|
219
214
|
msg = "[OK] Found #{matches} matches in #{files} files"
|
|
220
|
-
|
|
215
|
+
|
|
221
216
|
# Add truncation info if present
|
|
222
217
|
if result[:truncated] && result[:truncation_reason]
|
|
223
218
|
msg += " (truncated: #{result[:truncation_reason]})"
|
|
224
219
|
end
|
|
225
|
-
|
|
220
|
+
|
|
226
221
|
msg
|
|
227
222
|
end
|
|
228
223
|
end
|
|
@@ -275,12 +270,12 @@ module Clacky
|
|
|
275
270
|
|
|
276
271
|
def search_file(file, regex, context_lines, max_matches)
|
|
277
272
|
matches = []
|
|
278
|
-
|
|
273
|
+
|
|
279
274
|
# Use File.foreach for memory-efficient line-by-line reading
|
|
280
275
|
File.foreach(file, chomp: true).with_index do |line, index|
|
|
281
276
|
# Stop if we have enough matches for this file
|
|
282
277
|
break if matches.length >= max_matches
|
|
283
|
-
|
|
278
|
+
|
|
284
279
|
next unless line.match?(regex)
|
|
285
280
|
|
|
286
281
|
# Truncate long lines
|
|
@@ -315,10 +310,10 @@ module Clacky
|
|
|
315
310
|
(start_line..end_line).each do |i|
|
|
316
311
|
line_content = lines[i]
|
|
317
312
|
# Truncate long lines in context too
|
|
318
|
-
display_content = line_content.length > MAX_LINE_LENGTH ?
|
|
319
|
-
"#{line_content[0...MAX_LINE_LENGTH]}..." :
|
|
313
|
+
display_content = line_content.length > MAX_LINE_LENGTH ?
|
|
314
|
+
"#{line_content[0...MAX_LINE_LENGTH]}..." :
|
|
320
315
|
line_content
|
|
321
|
-
|
|
316
|
+
|
|
322
317
|
context << {
|
|
323
318
|
line_number: i + 1,
|
|
324
319
|
content: display_content,
|
|
@@ -32,20 +32,26 @@ module Clacky
|
|
|
32
32
|
required: ["command"]
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
def execute(command:, timeout: nil, max_output_lines: 1000)
|
|
35
|
+
def execute(command:, timeout: nil, max_output_lines: 1000, skip_safety_check: false)
|
|
36
36
|
# Get project root directory
|
|
37
37
|
project_root = Dir.pwd
|
|
38
38
|
|
|
39
39
|
begin
|
|
40
40
|
# 1. Extract timeout from command if it starts with "timeout N"
|
|
41
41
|
command, extracted_timeout = extract_timeout_from_command(command)
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
# Use extracted timeout if not explicitly provided
|
|
44
44
|
timeout ||= extracted_timeout
|
|
45
45
|
|
|
46
|
-
# 2. Use safety replacer to process command
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
# 2. Use safety replacer to process command (skip if user already confirmed)
|
|
47
|
+
if skip_safety_check
|
|
48
|
+
# User has confirmed, execute command as-is (no safety modifications)
|
|
49
|
+
safe_command = command
|
|
50
|
+
safety_replacer = nil
|
|
51
|
+
else
|
|
52
|
+
safety_replacer = CommandSafetyReplacer.new(project_root)
|
|
53
|
+
safe_command = safety_replacer.make_command_safe(command)
|
|
54
|
+
end
|
|
49
55
|
|
|
50
56
|
# 3. Calculate timeouts: soft_timeout is fixed at 5s, hard_timeout from timeout parameter
|
|
51
57
|
soft_timeout = 5
|
|
@@ -55,7 +61,7 @@ module Clacky
|
|
|
55
61
|
result = super(command: safe_command, soft_timeout: soft_timeout, hard_timeout: hard_timeout, max_output_lines: max_output_lines)
|
|
56
62
|
|
|
57
63
|
# 5. Enhance result information
|
|
58
|
-
enhance_result(result, command, safe_command)
|
|
64
|
+
enhance_result(result, command, safe_command, safety_replacer)
|
|
59
65
|
|
|
60
66
|
rescue SecurityError => e
|
|
61
67
|
# Security error, return friendly error message
|
|
@@ -145,9 +151,9 @@ module Clacky
|
|
|
145
151
|
end
|
|
146
152
|
end
|
|
147
153
|
|
|
148
|
-
def enhance_result(result, original_command, safe_command)
|
|
154
|
+
def enhance_result(result, original_command, safe_command, safety_replacer = nil)
|
|
149
155
|
# If command was replaced, add security information
|
|
150
|
-
if original_command != safe_command
|
|
156
|
+
if safety_replacer && original_command != safe_command
|
|
151
157
|
result[:security_enhanced] = true
|
|
152
158
|
result[:original_command] = original_command
|
|
153
159
|
result[:safe_command] = safe_command
|
data/lib/clacky/tools/shell.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "tmpdir"
|
|
3
4
|
require_relative "base"
|
|
4
5
|
|
|
5
6
|
module Clacky
|
|
@@ -35,7 +36,7 @@ module Clacky
|
|
|
35
36
|
INTERACTION_PATTERNS = [
|
|
36
37
|
[/\[Y\/n\]|\[y\/N\]|\(yes\/no\)|\(Y\/n\)|\(y\/N\)/i, 'confirmation'],
|
|
37
38
|
[/[Pp]assword\s*:\s*$|Enter password|enter password/, 'password'],
|
|
38
|
-
[/^\s*>>>\s*$|^\s*>>?\s*$|^irb\(.*\):\d+:\d+[>*]\s
|
|
39
|
+
[/^\s*>>>\s*$|^\s*>>?\s*$|^irb\(.*\):\d+:\d+[>*]\s*$|^\>\s*$/, 'repl'],
|
|
39
40
|
[/^\s*:\s*$|\(END\)|--More--|Press .* to continue|lines \d+-\d+/, 'pager'],
|
|
40
41
|
[/Are you sure|Continue\?|Proceed\?|Confirm|Overwrite/i, 'question'],
|
|
41
42
|
[/Enter\s+\w+:|Input\s+\w+:|Please enter|please provide/i, 'input'],
|
|
@@ -143,7 +144,7 @@ module Clacky
|
|
|
143
144
|
|
|
144
145
|
stdout_output = stdout_buffer.string
|
|
145
146
|
stderr_output = stderr_buffer.string
|
|
146
|
-
|
|
147
|
+
|
|
147
148
|
{
|
|
148
149
|
command: command,
|
|
149
150
|
stdout: truncate_output(stdout_output, max_output_lines),
|
|
@@ -157,7 +158,7 @@ module Clacky
|
|
|
157
158
|
rescue StandardError => e
|
|
158
159
|
stdout_output = stdout_buffer.string
|
|
159
160
|
stderr_output = "Error executing command: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
|
|
160
|
-
|
|
161
|
+
|
|
161
162
|
{
|
|
162
163
|
command: command,
|
|
163
164
|
stdout: truncate_output(stdout_output, max_output_lines),
|
|
@@ -253,10 +254,10 @@ module Clacky
|
|
|
253
254
|
# Truncate output to max_lines, adding a truncation notice if needed
|
|
254
255
|
def truncate_output(output, max_lines)
|
|
255
256
|
return output if output.nil? || output.empty?
|
|
256
|
-
|
|
257
|
+
|
|
257
258
|
lines = output.lines
|
|
258
259
|
return output if lines.length <= max_lines
|
|
259
|
-
|
|
260
|
+
|
|
260
261
|
truncated_lines = lines.first(max_lines)
|
|
261
262
|
truncation_notice = "\n\n... [Output truncated: showing #{max_lines} of #{lines.length} lines] ...\n"
|
|
262
263
|
truncated_lines.join + truncation_notice
|
|
@@ -293,69 +294,105 @@ module Clacky
|
|
|
293
294
|
|
|
294
295
|
# Format result for LLM consumption - limit output size to save tokens
|
|
295
296
|
# Maximum characters to include in LLM output
|
|
296
|
-
MAX_LLM_OUTPUT_CHARS =
|
|
297
|
-
|
|
297
|
+
MAX_LLM_OUTPUT_CHARS = 1000
|
|
298
|
+
|
|
298
299
|
def format_result_for_llm(result)
|
|
299
300
|
# Return error info as-is if command failed or timed out
|
|
300
301
|
return result if result[:error] || result[:state] == 'TIMEOUT' || result[:state] == 'WAITING_INPUT'
|
|
301
|
-
|
|
302
|
+
|
|
302
303
|
stdout = result[:stdout] || ""
|
|
303
304
|
stderr = result[:stderr] || ""
|
|
304
305
|
exit_code = result[:exit_code] || 0
|
|
305
|
-
|
|
306
|
+
|
|
306
307
|
# Build compact result with truncated output
|
|
307
308
|
compact = {
|
|
308
309
|
command: result[:command],
|
|
309
310
|
exit_code: exit_code,
|
|
310
311
|
success: result[:success]
|
|
311
312
|
}
|
|
312
|
-
|
|
313
|
-
# Add elapsed time if available
|
|
313
|
+
|
|
314
|
+
# Add elapsed time if available (keep original precision)
|
|
314
315
|
compact[:elapsed] = result[:elapsed] if result[:elapsed]
|
|
315
|
-
|
|
316
|
-
#
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
316
|
+
|
|
317
|
+
# Extract command name for temp file naming
|
|
318
|
+
command_name = extract_command_name(result[:command])
|
|
319
|
+
|
|
320
|
+
# Process stdout: truncate and optionally save to temp file
|
|
321
|
+
stdout_info = truncate_and_save(stdout, MAX_LLM_OUTPUT_CHARS, "stdout", command_name)
|
|
322
|
+
compact[:stdout] = stdout_info[:content]
|
|
323
|
+
compact[:stdout_full] = stdout_info[:temp_file] if stdout_info[:temp_file]
|
|
324
|
+
|
|
325
|
+
# Process stderr: truncate and optionally save to temp file
|
|
326
|
+
stderr_info = truncate_and_save(stderr, MAX_LLM_OUTPUT_CHARS, "stderr", command_name)
|
|
327
|
+
compact[:stderr] = stderr_info[:content]
|
|
328
|
+
compact[:stderr_full] = stderr_info[:temp_file] if stderr_info[:temp_file]
|
|
329
|
+
|
|
330
|
+
# Add output_truncated flag if present
|
|
331
|
+
compact[:output_truncated] = true if result[:output_truncated]
|
|
332
|
+
|
|
333
|
+
compact
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Extract command name from full command for temp file naming
|
|
337
|
+
def extract_command_name(command)
|
|
338
|
+
first_word = command.strip.split(/\s+/).first
|
|
339
|
+
File.basename(first_word, ".*")
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Truncate output for LLM and optionally save full content to temp file
|
|
343
|
+
def truncate_and_save(output, max_chars, label, command_name)
|
|
344
|
+
return { content: "", temp_file: nil } if output.empty?
|
|
345
|
+
|
|
346
|
+
return { content: output, temp_file: nil } if output.length <= max_chars
|
|
347
|
+
|
|
348
|
+
# Sanitize command name for file name
|
|
349
|
+
safe_name = command_name.gsub(/[^\w\-.]/, "_")[0...50]
|
|
350
|
+
|
|
351
|
+
# Use Ruby tmpdir for safe temp file creation
|
|
352
|
+
temp_dir = Dir.mktmpdir
|
|
353
|
+
temp_file = File.join(temp_dir, "#{safe_name}_#{Time.now.strftime("%Y%m%d_%H%M%S")}.output")
|
|
354
|
+
|
|
355
|
+
# Write full output to temp file
|
|
356
|
+
File.write(temp_file, output)
|
|
357
|
+
|
|
358
|
+
# For LLM display: show first and last lines to preserve structure
|
|
359
|
+
lines = output.lines
|
|
360
|
+
return { content: output, temp_file: nil } if lines.length <= 2
|
|
361
|
+
|
|
362
|
+
# Reserve 60 chars for truncation notice
|
|
363
|
+
available_chars = max_chars - 60
|
|
364
|
+
|
|
365
|
+
# Take first few lines
|
|
366
|
+
first_part = []
|
|
367
|
+
accumulated = 0
|
|
368
|
+
lines.each do |line|
|
|
369
|
+
break if accumulated + line.length > available_chars / 2
|
|
370
|
+
first_part << line
|
|
371
|
+
accumulated += line.length
|
|
339
372
|
end
|
|
340
|
-
|
|
341
|
-
#
|
|
342
|
-
|
|
343
|
-
|
|
373
|
+
|
|
374
|
+
# Take last few lines
|
|
375
|
+
last_part = []
|
|
376
|
+
accumulated = 0
|
|
377
|
+
lines.reverse_each do |line|
|
|
378
|
+
break if accumulated + line.length > available_chars / 2
|
|
379
|
+
last_part.unshift(line)
|
|
380
|
+
accumulated += line.length
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
total_lines = lines.length
|
|
384
|
+
|
|
385
|
+
# Create notice message based on label
|
|
386
|
+
if label == "stderr"
|
|
387
|
+
notice = "... [Error output truncated for LLM: showing #{first_part.length} of #{total_lines} lines, full content: #{temp_file} (use grep to search)] ..."
|
|
344
388
|
else
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if stderr.length <= MAX_LLM_OUTPUT_CHARS
|
|
348
|
-
compact[:stderr] = stderr
|
|
349
|
-
else
|
|
350
|
-
compact[:stderr] = stderr[0...MAX_LLM_OUTPUT_CHARS]
|
|
351
|
-
compact[:stderr] += "\n\n... [Error output truncated for LLM: showing #{MAX_LLM_OUTPUT_CHARS} of #{stderr.length} chars, #{stderr_line_count} lines] ...\n"
|
|
352
|
-
end
|
|
389
|
+
notice = "... [Output truncated for LLM: showing #{first_part.length} of #{total_lines} lines, full content: #{temp_file} (use grep to search)] ..."
|
|
353
390
|
end
|
|
354
|
-
|
|
355
|
-
#
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
391
|
+
|
|
392
|
+
# Combine with compact notice
|
|
393
|
+
content = first_part.join + "\n#{notice}\n" + last_part.join
|
|
394
|
+
|
|
395
|
+
{ content: content, temp_file: temp_file }
|
|
359
396
|
end
|
|
360
397
|
end
|
|
361
398
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "uri"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "fileutils"
|
|
5
7
|
|
|
6
8
|
module Clacky
|
|
7
9
|
module Tools
|
|
@@ -18,14 +20,14 @@ module Clacky
|
|
|
18
20
|
},
|
|
19
21
|
max_length: {
|
|
20
22
|
type: "integer",
|
|
21
|
-
description: "Maximum content length to return in characters (default:
|
|
22
|
-
default:
|
|
23
|
+
description: "Maximum content length to return in characters (default: 3000)",
|
|
24
|
+
default: 3000
|
|
23
25
|
}
|
|
24
26
|
},
|
|
25
27
|
required: %w[url]
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
def execute(url:, max_length:
|
|
30
|
+
def execute(url:, max_length: 3000)
|
|
29
31
|
# Validate URL
|
|
30
32
|
begin
|
|
31
33
|
uri = URI.parse(url)
|
|
@@ -46,7 +48,7 @@ module Clacky
|
|
|
46
48
|
|
|
47
49
|
# Parse HTML if it's an HTML page
|
|
48
50
|
if content_type.include?("text/html")
|
|
49
|
-
result = parse_html(content, max_length)
|
|
51
|
+
result = parse_html(content, max_length, url)
|
|
50
52
|
result[:url] = url
|
|
51
53
|
result[:content_type] = content_type
|
|
52
54
|
result[:status_code] = response.code.to_i
|
|
@@ -54,21 +56,45 @@ module Clacky
|
|
|
54
56
|
result
|
|
55
57
|
else
|
|
56
58
|
# For non-HTML content, return raw text
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
url: url,
|
|
60
|
-
content_type: content_type,
|
|
61
|
-
status_code: response.code.to_i,
|
|
62
|
-
content: truncated_content,
|
|
63
|
-
truncated: content.length > max_length,
|
|
64
|
-
error: nil
|
|
65
|
-
}
|
|
59
|
+
result = handle_raw_content(content, max_length, url, content_type, response.code.to_i)
|
|
60
|
+
result
|
|
66
61
|
end
|
|
67
62
|
rescue StandardError => e
|
|
68
63
|
{ error: "Failed to fetch URL: #{e.message}" }
|
|
69
64
|
end
|
|
70
65
|
end
|
|
71
66
|
|
|
67
|
+
def handle_raw_content(content, max_length, url, content_type, status_code)
|
|
68
|
+
truncated = content.length > max_length
|
|
69
|
+
temp_file = nil
|
|
70
|
+
|
|
71
|
+
if truncated
|
|
72
|
+
temp_dir = Dir.mktmpdir
|
|
73
|
+
domain = extract_domain(url)
|
|
74
|
+
safe_name = domain.gsub(/[^\w\-.]/, '_')[0...50]
|
|
75
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
76
|
+
temp_file = File.join(temp_dir, "#{safe_name}_#{timestamp}.txt")
|
|
77
|
+
File.write(temp_file, content)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
url: url,
|
|
82
|
+
content_type: content_type,
|
|
83
|
+
status_code: status_code,
|
|
84
|
+
content: content[0, max_length],
|
|
85
|
+
truncated: truncated,
|
|
86
|
+
temp_file: temp_file,
|
|
87
|
+
error: nil
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def extract_domain(url)
|
|
92
|
+
uri = URI.parse(url)
|
|
93
|
+
uri.host || url.gsub(/[^\w\-.]/, '_')
|
|
94
|
+
rescue
|
|
95
|
+
url.gsub(/[^\w\-.]/, '_')
|
|
96
|
+
end
|
|
97
|
+
|
|
72
98
|
def fetch_url(uri)
|
|
73
99
|
# Follow redirects (max 5)
|
|
74
100
|
redirects = 0
|
|
@@ -97,7 +123,7 @@ module Clacky
|
|
|
97
123
|
end
|
|
98
124
|
end
|
|
99
125
|
|
|
100
|
-
def parse_html(html, max_length)
|
|
126
|
+
def parse_html(html, max_length, url = nil)
|
|
101
127
|
# Extract title
|
|
102
128
|
title = ""
|
|
103
129
|
if html =~ %r{<title[^>]*>(.*?)</title>}mi
|
|
@@ -122,15 +148,25 @@ module Clacky
|
|
|
122
148
|
# Clean up whitespace
|
|
123
149
|
text = text.gsub(/\s+/, " ").strip
|
|
124
150
|
|
|
125
|
-
#
|
|
151
|
+
# Check if we need to save to temp file
|
|
126
152
|
truncated = text.length > max_length
|
|
127
|
-
|
|
153
|
+
temp_file = nil
|
|
154
|
+
|
|
155
|
+
if truncated
|
|
156
|
+
temp_dir = Dir.mktmpdir
|
|
157
|
+
domain = url ? extract_domain(url) : "web_fetch"
|
|
158
|
+
safe_name = domain.gsub(/[^\w\-.]/, '_')[0...50]
|
|
159
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
160
|
+
temp_file = File.join(temp_dir, "#{safe_name}_#{timestamp}.txt")
|
|
161
|
+
File.write(temp_file, text)
|
|
162
|
+
end
|
|
128
163
|
|
|
129
164
|
{
|
|
130
165
|
title: title,
|
|
131
166
|
description: description,
|
|
132
|
-
content: text,
|
|
133
|
-
truncated: truncated
|
|
167
|
+
content: text[0, max_length],
|
|
168
|
+
truncated: truncated,
|
|
169
|
+
temp_file: temp_file
|
|
134
170
|
}
|
|
135
171
|
end
|
|
136
172
|
|
|
@@ -156,6 +192,33 @@ module Clacky
|
|
|
156
192
|
"[OK] Fetched: #{display_title}"
|
|
157
193
|
end
|
|
158
194
|
end
|
|
195
|
+
|
|
196
|
+
# Format result for LLM consumption - return compact version to save tokens
|
|
197
|
+
def format_result_for_llm(result)
|
|
198
|
+
# Return error as-is
|
|
199
|
+
return result if result[:error]
|
|
200
|
+
|
|
201
|
+
# Build compact result
|
|
202
|
+
compact = {
|
|
203
|
+
url: result[:url],
|
|
204
|
+
title: result[:title],
|
|
205
|
+
description: result[:description],
|
|
206
|
+
status_code: result[:status_code]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Add truncated notice and temp file info if content was truncated
|
|
210
|
+
if result[:truncated] && result[:temp_file]
|
|
211
|
+
compact[:content] = result[:content]
|
|
212
|
+
compact[:truncated] = true
|
|
213
|
+
compact[:temp_file] = result[:temp_file]
|
|
214
|
+
compact[:message] = "[Content truncated - full content saved to temp file. Use file_reader to read it if needed.]"
|
|
215
|
+
else
|
|
216
|
+
compact[:content] = result[:content]
|
|
217
|
+
compact[:truncated] = result[:truncated] || false
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
compact
|
|
221
|
+
end
|
|
159
222
|
end
|
|
160
223
|
end
|
|
161
224
|
end
|