swarm_sdk 2.7.14 → 3.0.0.alpha1
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/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
- data/lib/swarm_sdk/v3/agent.rb +1165 -0
- data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
- data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
- data/lib/swarm_sdk/v3/configuration.rb +490 -0
- data/lib/swarm_sdk/v3/debug_log.rb +86 -0
- data/lib/swarm_sdk/v3/event_stream.rb +130 -0
- data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
- data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
- data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
- data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
- data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
- data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
- data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
- data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
- data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
- data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
- data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
- data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
- data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
- data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
- data/lib/swarm_sdk/v3/memory/card.rb +206 -0
- data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
- data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
- data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
- data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
- data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
- data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
- data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
- data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
- data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
- data/lib/swarm_sdk/v3/memory/store.rb +489 -0
- data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
- data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
- data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
- data/lib/swarm_sdk/v3/tools/base.rb +80 -0
- data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
- data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
- data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
- data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
- data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
- data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
- data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
- data/lib/swarm_sdk/v3/tools/read.rb +181 -0
- data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
- data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
- data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
- data/lib/swarm_sdk/v3/tools/think.rb +88 -0
- data/lib/swarm_sdk/v3/tools/write.rb +87 -0
- data/lib/swarm_sdk/v3.rb +145 -0
- metadata +83 -148
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -705
- data/lib/swarm_sdk/agent/chat.rb +0 -1438
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
- data/lib/swarm_sdk/agent/context.rb +0 -115
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -588
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
- data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
- data/lib/swarm_sdk/agent_registry.rb +0 -146
- data/lib/swarm_sdk/builders/base_builder.rb +0 -558
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/config.rb +0 -368
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -285
- data/lib/swarm_sdk/configuration.rb +0 -165
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
- data/lib/swarm_sdk/defaults.rb +0 -251
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -44002
- data/lib/swarm_sdk/models.rb +0 -161
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -248
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -241
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -446
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
- data/lib/swarm_sdk/swarm.rb +0 -973
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/base.rb +0 -63
- data/lib/swarm_sdk/tools/bash.rb +0 -280
- data/lib/swarm_sdk/tools/clock.rb +0 -46
- data/lib/swarm_sdk/tools/delegate.rb +0 -389
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -100
- data/lib/swarm_sdk/tools/todo_write.rb +0 -237
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/transcript_builder.rb +0 -278
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
- data/lib/swarm_sdk/workflow/builder.rb +0 -227
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
- data/lib/swarm_sdk/workflow.rb +0 -589
- data/lib/swarm_sdk.rb +0 -721
data/lib/swarm_sdk/tools/read.rb
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
# Read tool for reading file contents from the filesystem
|
|
6
|
-
#
|
|
7
|
-
# Supports reading entire files or specific line ranges with line numbers.
|
|
8
|
-
# Provides system reminders to guide proper usage.
|
|
9
|
-
# Tracks reads per agent for enforcing read-before-write/edit rules.
|
|
10
|
-
class Read < Base
|
|
11
|
-
include PathResolver
|
|
12
|
-
|
|
13
|
-
# NOTE: Line length and limit now accessed via SwarmSDK.config
|
|
14
|
-
|
|
15
|
-
# List of available document converters
|
|
16
|
-
CONVERTERS = [
|
|
17
|
-
DocumentConverters::PdfConverter,
|
|
18
|
-
DocumentConverters::DocxConverter,
|
|
19
|
-
DocumentConverters::XlsxConverter,
|
|
20
|
-
].freeze
|
|
21
|
-
|
|
22
|
-
# Build dynamic description based on available gems
|
|
23
|
-
available_formats = CONVERTERS.select(&:available?).map(&:format_name)
|
|
24
|
-
doc_support_text = if available_formats.any?
|
|
25
|
-
"- Document files: #{available_formats.join(", ")} are converted to text"
|
|
26
|
-
else
|
|
27
|
-
""
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
31
|
-
class << self
|
|
32
|
-
def creation_requirements
|
|
33
|
-
[:agent_name, :directory]
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
description <<~DESC
|
|
38
|
-
Reads a file from the local filesystem. You can access any file directly by using this tool.
|
|
39
|
-
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid.
|
|
40
|
-
It is okay to read a file that does not exist; an error will be returned.
|
|
41
|
-
|
|
42
|
-
Supports text, binary, and document files:
|
|
43
|
-
- Text files are returned with line numbers
|
|
44
|
-
- Binary files (images) are returned as visual content for analysis
|
|
45
|
-
- Supported image formats: PNG, JPG, GIF, WEBP, BMP, TIFF, SVG, ICO
|
|
46
|
-
#{doc_support_text}
|
|
47
|
-
|
|
48
|
-
IMPORTANT - Path Handling:
|
|
49
|
-
- Relative paths (e.g., "tmp/file.txt", "src/main.rb") are resolved relative to your agent's working directory
|
|
50
|
-
- Absolute paths (e.g., "/tmp/file.txt", "/etc/passwd") are treated as system absolute paths
|
|
51
|
-
- When the user says "tmp/file.txt" they mean the tmp directory in your working directory, NOT /tmp
|
|
52
|
-
- Only use absolute paths (starting with /) when explicitly referring to system-level paths
|
|
53
|
-
DESC
|
|
54
|
-
|
|
55
|
-
param :file_path,
|
|
56
|
-
type: "string",
|
|
57
|
-
desc: "Path to the file. Use relative paths (e.g., 'tmp/file.txt') for files in your working directory, or absolute paths (e.g., '/etc/passwd') for system files.",
|
|
58
|
-
required: true
|
|
59
|
-
|
|
60
|
-
param :offset,
|
|
61
|
-
type: "integer",
|
|
62
|
-
desc: "The line number to start reading from (1-indexed). Only provide if the file is too large to read at once.",
|
|
63
|
-
required: false
|
|
64
|
-
|
|
65
|
-
param :limit,
|
|
66
|
-
type: "integer",
|
|
67
|
-
desc: "The number of lines to read. Only provide if the file is too large to read at once.",
|
|
68
|
-
required: false
|
|
69
|
-
|
|
70
|
-
# Initialize the Read tool for a specific agent
|
|
71
|
-
#
|
|
72
|
-
# @param agent_name [Symbol, String] The agent identifier
|
|
73
|
-
# @param directory [String] Agent's working directory
|
|
74
|
-
def initialize(agent_name:, directory:)
|
|
75
|
-
super()
|
|
76
|
-
initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Override name to return simple "Read" instead of full class path
|
|
80
|
-
def name
|
|
81
|
-
"Read"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def execute(file_path:, offset: nil, limit: nil)
|
|
85
|
-
# Validate file path
|
|
86
|
-
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
|
87
|
-
|
|
88
|
-
# CRITICAL: Resolve path against agent directory
|
|
89
|
-
resolved_path = resolve_path(file_path)
|
|
90
|
-
|
|
91
|
-
unless File.exist?(resolved_path)
|
|
92
|
-
return validation_error("File does not exist: #{file_path}")
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Check if it's a directory
|
|
96
|
-
if File.directory?(resolved_path)
|
|
97
|
-
return validation_error("Path is a directory, not a file. Use Bash with ls to read directories.")
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Check if it's a document and try to convert it
|
|
101
|
-
converter = find_converter_for_file(resolved_path)
|
|
102
|
-
if converter
|
|
103
|
-
result = converter.new.convert(resolved_path)
|
|
104
|
-
# For document files, register the converted text content
|
|
105
|
-
# Extract text from result (may be wrapped in system-reminder tags)
|
|
106
|
-
if result.is_a?(String)
|
|
107
|
-
# Remove system-reminder wrapper if present to get clean text for digest
|
|
108
|
-
text_content = result.gsub(%r{<system-reminder>.*?</system-reminder>}m, "").strip
|
|
109
|
-
Stores::ReadTracker.register_read(@agent_name, resolved_path, text_content)
|
|
110
|
-
end
|
|
111
|
-
return result
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Try to read as text, handle binary files separately
|
|
115
|
-
content = read_file_content(resolved_path)
|
|
116
|
-
|
|
117
|
-
# If content is a Content object (binary file), track with binary digest and return
|
|
118
|
-
if content.is_a?(RubyLLM::Content)
|
|
119
|
-
# For binary files, read raw bytes for digest
|
|
120
|
-
binary_content = File.binread(resolved_path)
|
|
121
|
-
Stores::ReadTracker.register_read(@agent_name, resolved_path, binary_content)
|
|
122
|
-
return content
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Return early if we got an error message or system reminder
|
|
126
|
-
return content if content.is_a?(String) && (content.start_with?("Error:") || content.start_with?("<system-reminder>"))
|
|
127
|
-
|
|
128
|
-
# At this point, we have valid text content - register the read with digest
|
|
129
|
-
Stores::ReadTracker.register_read(@agent_name, resolved_path, content)
|
|
130
|
-
|
|
131
|
-
# Check if file is empty
|
|
132
|
-
if content.empty?
|
|
133
|
-
return format_with_reminder(
|
|
134
|
-
"",
|
|
135
|
-
"<system-reminder>Warning: This file exists but has empty contents. This may be intentional or indicate an issue.</system-reminder>",
|
|
136
|
-
)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Split into lines and apply offset/limit
|
|
140
|
-
lines = content.lines
|
|
141
|
-
total_lines = lines.count
|
|
142
|
-
|
|
143
|
-
# Apply offset if specified (1-indexed)
|
|
144
|
-
start_line = offset ? offset - 1 : 0
|
|
145
|
-
start_line = [start_line, 0].max # Ensure non-negative
|
|
146
|
-
|
|
147
|
-
if start_line >= total_lines
|
|
148
|
-
return validation_error("Offset #{offset} exceeds file length (#{total_lines} lines)")
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
lines = lines.drop(start_line)
|
|
152
|
-
|
|
153
|
-
# Apply limit if specified, otherwise use default
|
|
154
|
-
default_limit = SwarmSDK.config.read_line_limit
|
|
155
|
-
effective_limit = limit || default_limit
|
|
156
|
-
lines = lines.take(effective_limit)
|
|
157
|
-
truncated = limit.nil? && total_lines > default_limit
|
|
158
|
-
|
|
159
|
-
# Format with line numbers (cat -n style)
|
|
160
|
-
max_line_length = SwarmSDK.config.line_character_limit
|
|
161
|
-
output_lines = lines.each_with_index.map do |line, idx|
|
|
162
|
-
line_number = start_line + idx + 1
|
|
163
|
-
display_line = line.chomp
|
|
164
|
-
|
|
165
|
-
# Truncate long lines
|
|
166
|
-
if display_line.length > max_line_length
|
|
167
|
-
display_line = display_line[0...max_line_length]
|
|
168
|
-
display_line += "... (line truncated)"
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Add line indicator for better readability
|
|
172
|
-
"#{line_number.to_s.rjust(6)}→#{display_line}"
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
output = output_lines.join("\n")
|
|
176
|
-
|
|
177
|
-
# Add system reminder about usage
|
|
178
|
-
reminder = build_system_reminder(file_path, truncated, total_lines)
|
|
179
|
-
format_with_reminder(output, reminder)
|
|
180
|
-
rescue StandardError => e
|
|
181
|
-
error("Unexpected error reading file: #{e.class.name} - #{e.message}")
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
private
|
|
185
|
-
|
|
186
|
-
# Find the appropriate converter for a file based on extension
|
|
187
|
-
def find_converter_for_file(file_path)
|
|
188
|
-
ext = File.extname(file_path).downcase
|
|
189
|
-
CONVERTERS.find { |converter| converter.extensions.include?(ext) }
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def format_with_reminder(content, reminder)
|
|
193
|
-
return content if reminder.nil? || reminder.empty?
|
|
194
|
-
|
|
195
|
-
[content, "", reminder].join("\n")
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def build_system_reminder(_file_path, truncated, total_lines)
|
|
199
|
-
reminders = []
|
|
200
|
-
|
|
201
|
-
reminders << "<system-reminder>"
|
|
202
|
-
reminders << "Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior."
|
|
203
|
-
|
|
204
|
-
if truncated
|
|
205
|
-
reminders << ""
|
|
206
|
-
reminders << "Note: This file has #{total_lines} lines but only the first #{SwarmSDK.config.read_line_limit} lines are shown. Use the offset and limit parameters to read additional sections if needed."
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
reminders << "</system-reminder>"
|
|
210
|
-
|
|
211
|
-
reminders.join("\n")
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def read_file_content(file_path)
|
|
215
|
-
content = File.read(file_path, encoding: "UTF-8")
|
|
216
|
-
|
|
217
|
-
# Check if the content is valid UTF-8
|
|
218
|
-
unless content.valid_encoding?
|
|
219
|
-
# Binary file detected
|
|
220
|
-
if supported_binary_file?(file_path)
|
|
221
|
-
return RubyLLM::Content.new("File: #{File.basename(file_path)}", file_path)
|
|
222
|
-
else
|
|
223
|
-
return "Error: File contains binary data and cannot be displayed as text. This may be an executable or other unsupported binary file."
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
content
|
|
228
|
-
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
229
|
-
# Binary file detected
|
|
230
|
-
if supported_binary_file?(file_path)
|
|
231
|
-
RubyLLM::Content.new("File: #{File.basename(file_path)}", file_path)
|
|
232
|
-
else
|
|
233
|
-
"Error: File contains binary data and cannot be displayed as text. This may be an executable or other unsupported binary file."
|
|
234
|
-
end
|
|
235
|
-
rescue Errno::EACCES
|
|
236
|
-
error("Permission denied: Cannot read file '#{file_path}'")
|
|
237
|
-
rescue StandardError => e
|
|
238
|
-
error("Failed to read file: #{e.message}")
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def supported_binary_file?(file_path)
|
|
242
|
-
ext = File.extname(file_path).downcase
|
|
243
|
-
# Supported binary file types that can be sent to the model
|
|
244
|
-
# Images only - documents are converted to text
|
|
245
|
-
supported_formats = [
|
|
246
|
-
".png",
|
|
247
|
-
".jpg",
|
|
248
|
-
".jpeg",
|
|
249
|
-
".gif",
|
|
250
|
-
".webp",
|
|
251
|
-
".bmp",
|
|
252
|
-
".tiff",
|
|
253
|
-
".tif",
|
|
254
|
-
".svg",
|
|
255
|
-
".ico",
|
|
256
|
-
]
|
|
257
|
-
supported_formats.include?(ext)
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
end
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
# Registry for built-in SwarmSDK tools
|
|
6
|
-
#
|
|
7
|
-
# Maps tool names (symbols) to their RubyLLM::Tool classes.
|
|
8
|
-
# Provides validation, lookup, and factory functionality for tool registration.
|
|
9
|
-
#
|
|
10
|
-
# ## Tool Creation Pattern
|
|
11
|
-
#
|
|
12
|
-
# Tools register themselves with their creation requirements via the `tool_factory` method.
|
|
13
|
-
# This eliminates the need for a giant case statement in ToolConfigurator.
|
|
14
|
-
#
|
|
15
|
-
# Tools fall into three categories:
|
|
16
|
-
# 1. **No params**: Simple tools with no initialization requirements (Think, Clock)
|
|
17
|
-
# 2. **Directory only**: Tools needing working directory (Bash, Grep, Glob)
|
|
18
|
-
# 3. **Agent context**: Tools needing agent tracking (Read, Write, Edit, MultiEdit)
|
|
19
|
-
# 4. **Scratchpad**: Tools needing scratchpad storage instance
|
|
20
|
-
#
|
|
21
|
-
# @example Adding a new tool with creation requirements
|
|
22
|
-
# # In the tool class:
|
|
23
|
-
# class MyTool < RubyLLM::Tool
|
|
24
|
-
# def self.creation_requirements
|
|
25
|
-
# [:agent_name, :directory]
|
|
26
|
-
# end
|
|
27
|
-
# end
|
|
28
|
-
#
|
|
29
|
-
# # In registry:
|
|
30
|
-
# BUILTIN_TOOLS = {
|
|
31
|
-
# MyTool: SwarmSDK::Tools::MyTool,
|
|
32
|
-
# }
|
|
33
|
-
#
|
|
34
|
-
# Note: Plugin-provided tools (e.g., memory tools) are NOT in this registry.
|
|
35
|
-
# They are registered via SwarmSDK::PluginRegistry instead.
|
|
36
|
-
class Registry
|
|
37
|
-
# All available built-in tools
|
|
38
|
-
#
|
|
39
|
-
# Maps tool names to their classes. The class must respond to `creation_requirements`
|
|
40
|
-
# to specify what parameters are needed for instantiation.
|
|
41
|
-
BUILTIN_TOOLS = {
|
|
42
|
-
Read: SwarmSDK::Tools::Read,
|
|
43
|
-
Write: SwarmSDK::Tools::Write,
|
|
44
|
-
Edit: SwarmSDK::Tools::Edit,
|
|
45
|
-
MultiEdit: SwarmSDK::Tools::MultiEdit,
|
|
46
|
-
Bash: SwarmSDK::Tools::Bash,
|
|
47
|
-
Grep: SwarmSDK::Tools::Grep,
|
|
48
|
-
Glob: SwarmSDK::Tools::Glob,
|
|
49
|
-
TodoWrite: SwarmSDK::Tools::TodoWrite,
|
|
50
|
-
ScratchpadWrite: :scratchpad, # Requires scratchpad storage instance
|
|
51
|
-
ScratchpadRead: :scratchpad, # Requires scratchpad storage instance
|
|
52
|
-
ScratchpadList: :scratchpad, # Requires scratchpad storage instance
|
|
53
|
-
Think: SwarmSDK::Tools::Think,
|
|
54
|
-
WebFetch: SwarmSDK::Tools::WebFetch,
|
|
55
|
-
Clock: SwarmSDK::Tools::Clock,
|
|
56
|
-
}.freeze
|
|
57
|
-
|
|
58
|
-
class << self
|
|
59
|
-
# Get tool class by name
|
|
60
|
-
#
|
|
61
|
-
# Note: Plugin-provided tools are NOT returned by this method.
|
|
62
|
-
# They are managed by SwarmSDK::PluginRegistry instead.
|
|
63
|
-
#
|
|
64
|
-
# @param name [Symbol, String] Tool name
|
|
65
|
-
# @return [Class, Symbol, nil] Tool class, :scratchpad marker, or nil if not found
|
|
66
|
-
def get(name)
|
|
67
|
-
name_sym = name.to_sym
|
|
68
|
-
BUILTIN_TOOLS[name_sym]
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Create a tool instance using the Factory Pattern
|
|
72
|
-
#
|
|
73
|
-
# Uses the tool's `creation_requirements` class method to determine
|
|
74
|
-
# what parameters to pass to the constructor.
|
|
75
|
-
#
|
|
76
|
-
# @param name [Symbol, String] Tool name
|
|
77
|
-
# @param context [Hash] Available context for tool creation
|
|
78
|
-
# @option context [Symbol] :agent_name Agent identifier
|
|
79
|
-
# @option context [String] :directory Agent's working directory
|
|
80
|
-
# @option context [Object] :scratchpad_storage Scratchpad storage instance
|
|
81
|
-
# @return [RubyLLM::Tool] Instantiated tool
|
|
82
|
-
# @raise [ConfigurationError] If tool is unknown or has unmet requirements
|
|
83
|
-
def create(name, context = {})
|
|
84
|
-
name_sym = name.to_sym
|
|
85
|
-
tool_entry = BUILTIN_TOOLS[name_sym]
|
|
86
|
-
|
|
87
|
-
raise ConfigurationError, "Unknown tool: #{name}" unless tool_entry
|
|
88
|
-
|
|
89
|
-
# Handle scratchpad tools specially (they use factory methods)
|
|
90
|
-
if tool_entry == :scratchpad
|
|
91
|
-
return create_scratchpad_tool(name_sym, context[:scratchpad_storage])
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Get the tool class and its requirements
|
|
95
|
-
tool_class = tool_entry
|
|
96
|
-
|
|
97
|
-
# Check if tool defines creation requirements
|
|
98
|
-
if tool_class.respond_to?(:creation_requirements)
|
|
99
|
-
requirements = tool_class.creation_requirements
|
|
100
|
-
params = extract_params(requirements, context, name)
|
|
101
|
-
tool_class.new(**params)
|
|
102
|
-
else
|
|
103
|
-
# No requirements - simple instantiation
|
|
104
|
-
tool_class.new
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Get multiple tool classes by names
|
|
109
|
-
#
|
|
110
|
-
# @param names [Array<Symbol, String>] Tool names
|
|
111
|
-
# @return [Array<Class>] Array of tool classes
|
|
112
|
-
# @raise [ConfigurationError] If any tool name is invalid
|
|
113
|
-
def get_many(names)
|
|
114
|
-
names.map do |name|
|
|
115
|
-
tool_class = get(name)
|
|
116
|
-
unless tool_class
|
|
117
|
-
raise ConfigurationError,
|
|
118
|
-
"Unknown tool: #{name}. Available tools: #{available_names.join(", ")}"
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
tool_class
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Check if a tool exists
|
|
126
|
-
#
|
|
127
|
-
# Note: Only checks built-in tools. Plugin-provided tools are checked
|
|
128
|
-
# via SwarmSDK::PluginRegistry.plugin_tool?() instead.
|
|
129
|
-
#
|
|
130
|
-
# @param name [Symbol, String] Tool name
|
|
131
|
-
# @return [Boolean]
|
|
132
|
-
def exists?(name)
|
|
133
|
-
name_sym = name.to_sym
|
|
134
|
-
BUILTIN_TOOLS.key?(name_sym)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Get all available built-in tool names
|
|
138
|
-
#
|
|
139
|
-
# Note: Does NOT include plugin-provided tools. To get all available tools
|
|
140
|
-
# including plugins, combine with SwarmSDK::PluginRegistry.tools.
|
|
141
|
-
#
|
|
142
|
-
# @return [Array<Symbol>]
|
|
143
|
-
def available_names
|
|
144
|
-
BUILTIN_TOOLS.keys
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Validate tool names
|
|
148
|
-
#
|
|
149
|
-
# @param names [Array<Symbol, String>] Tool names to validate
|
|
150
|
-
# @return [Array<Symbol>] Invalid tool names
|
|
151
|
-
def validate(names)
|
|
152
|
-
names.reject { |name| exists?(name) }
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
private
|
|
156
|
-
|
|
157
|
-
# Extract required parameters from context
|
|
158
|
-
#
|
|
159
|
-
# @param requirements [Array<Symbol>] Required parameter names
|
|
160
|
-
# @param context [Hash] Available context
|
|
161
|
-
# @param tool_name [Symbol] Tool name for error messages
|
|
162
|
-
# @return [Hash] Parameters to pass to tool constructor
|
|
163
|
-
# @raise [ConfigurationError] If required parameter is missing
|
|
164
|
-
def extract_params(requirements, context, tool_name)
|
|
165
|
-
params = {}
|
|
166
|
-
|
|
167
|
-
requirements.each do |req|
|
|
168
|
-
unless context.key?(req)
|
|
169
|
-
raise ConfigurationError,
|
|
170
|
-
"Tool #{tool_name} requires #{req} but it was not provided in context"
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
params[req] = context[req]
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
params
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Create a scratchpad tool using its factory method
|
|
180
|
-
#
|
|
181
|
-
# @param name [Symbol] Scratchpad tool name
|
|
182
|
-
# @param storage [Object] Scratchpad storage instance
|
|
183
|
-
# @return [RubyLLM::Tool] Instantiated scratchpad tool
|
|
184
|
-
# @raise [ConfigurationError] If storage is not provided
|
|
185
|
-
def create_scratchpad_tool(name, storage)
|
|
186
|
-
unless storage
|
|
187
|
-
raise ConfigurationError,
|
|
188
|
-
"Scratchpad tool #{name} requires scratchpad_storage in context"
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
case name
|
|
192
|
-
when :ScratchpadWrite
|
|
193
|
-
Tools::Scratchpad::ScratchpadWrite.create_for_scratchpad(storage)
|
|
194
|
-
when :ScratchpadRead
|
|
195
|
-
Tools::Scratchpad::ScratchpadRead.create_for_scratchpad(storage)
|
|
196
|
-
when :ScratchpadList
|
|
197
|
-
Tools::Scratchpad::ScratchpadList.create_for_scratchpad(storage)
|
|
198
|
-
else
|
|
199
|
-
raise ConfigurationError, "Unknown scratchpad tool: #{name}"
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module Scratchpad
|
|
6
|
-
# Tool for listing scratchpad entries
|
|
7
|
-
#
|
|
8
|
-
# Shows all entries in the shared scratchpad with their metadata.
|
|
9
|
-
# All agents in the swarm share the same scratchpad.
|
|
10
|
-
class ScratchpadList < Base
|
|
11
|
-
define_method(:name) { "ScratchpadList" }
|
|
12
|
-
|
|
13
|
-
description <<~DESC
|
|
14
|
-
List all entries in scratchpad with their metadata.
|
|
15
|
-
|
|
16
|
-
## When to Use ScratchpadList
|
|
17
|
-
|
|
18
|
-
Use ScratchpadList to:
|
|
19
|
-
- Discover what content is available in the scratchpad
|
|
20
|
-
- Check what other agents have stored
|
|
21
|
-
- Find relevant entries before reading them
|
|
22
|
-
- Review all stored outputs and analysis
|
|
23
|
-
- Check entry sizes and last update times
|
|
24
|
-
|
|
25
|
-
## Best Practices
|
|
26
|
-
|
|
27
|
-
- Use this before ScratchpadRead if you don't know what's stored
|
|
28
|
-
- Filter by prefix to narrow down results (e.g., 'notes/' lists all notes)
|
|
29
|
-
- Shows path, title, size, and last updated time for each entry
|
|
30
|
-
- Any agent can see all scratchpad entries
|
|
31
|
-
- Helps coordinate multi-agent workflows
|
|
32
|
-
|
|
33
|
-
## Examples
|
|
34
|
-
|
|
35
|
-
- List all entries: (no prefix parameter)
|
|
36
|
-
- List notes only: prefix='notes/'
|
|
37
|
-
- List analysis results: prefix='analysis/'
|
|
38
|
-
DESC
|
|
39
|
-
|
|
40
|
-
param :prefix,
|
|
41
|
-
desc: "Optional prefix to filter entries (e.g., 'notes/' to list all entries under notes/)",
|
|
42
|
-
required: false
|
|
43
|
-
|
|
44
|
-
class << self
|
|
45
|
-
# Create a ScratchpadList tool for a specific scratchpad storage instance
|
|
46
|
-
#
|
|
47
|
-
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
48
|
-
# @return [ScratchpadList] Tool instance
|
|
49
|
-
def create_for_scratchpad(scratchpad_storage)
|
|
50
|
-
new(scratchpad_storage)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Initialize with scratchpad storage instance
|
|
55
|
-
#
|
|
56
|
-
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
57
|
-
def initialize(scratchpad_storage)
|
|
58
|
-
super() # Call RubyLLM::Tool's initialize
|
|
59
|
-
@scratchpad_storage = scratchpad_storage
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Execute the tool
|
|
63
|
-
#
|
|
64
|
-
# @param prefix [String, nil] Optional prefix to filter entries
|
|
65
|
-
# @return [String] Formatted list of entries
|
|
66
|
-
def execute(prefix: nil)
|
|
67
|
-
entries = scratchpad_storage.list(prefix: prefix)
|
|
68
|
-
|
|
69
|
-
if entries.empty?
|
|
70
|
-
prefix_msg = prefix ? " with prefix '#{prefix}'" : ""
|
|
71
|
-
return "No entries found in scratchpad#{prefix_msg}"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
result = []
|
|
75
|
-
prefix_msg = prefix ? " with prefix '#{prefix}'" : ""
|
|
76
|
-
result << "Scratchpad entries#{prefix_msg} (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
|
|
77
|
-
result << ""
|
|
78
|
-
|
|
79
|
-
entries.each do |entry|
|
|
80
|
-
time_str = entry[:updated_at].strftime("%Y-%m-%d %H:%M:%S")
|
|
81
|
-
result << " scratchpad://#{entry[:path]}"
|
|
82
|
-
result << " Title: #{entry[:title]}"
|
|
83
|
-
result << " Size: #{format_bytes(entry[:size])}"
|
|
84
|
-
result << " Updated: #{time_str}"
|
|
85
|
-
result << ""
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
result.join("\n").rstrip
|
|
89
|
-
rescue ArgumentError => e
|
|
90
|
-
validation_error(e.message)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
private
|
|
94
|
-
|
|
95
|
-
attr_reader :scratchpad_storage
|
|
96
|
-
|
|
97
|
-
def validation_error(message)
|
|
98
|
-
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Format bytes to human-readable size
|
|
102
|
-
#
|
|
103
|
-
# @param bytes [Integer] Number of bytes
|
|
104
|
-
# @return [String] Formatted size
|
|
105
|
-
def format_bytes(bytes)
|
|
106
|
-
if bytes >= 1_000_000
|
|
107
|
-
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
|
108
|
-
elsif bytes >= 1_000
|
|
109
|
-
"#{(bytes.to_f / 1_000).round(1)}KB"
|
|
110
|
-
else
|
|
111
|
-
"#{bytes}B"
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module Scratchpad
|
|
6
|
-
# Tool for reading content from scratchpad storage
|
|
7
|
-
#
|
|
8
|
-
# Retrieves content stored by any agent using scratchpad_write.
|
|
9
|
-
# All agents in the swarm share the same scratchpad.
|
|
10
|
-
class ScratchpadRead < Base
|
|
11
|
-
define_method(:name) { "ScratchpadRead" }
|
|
12
|
-
|
|
13
|
-
description <<~DESC
|
|
14
|
-
Read content from scratchpad.
|
|
15
|
-
|
|
16
|
-
## When to Use ScratchpadRead
|
|
17
|
-
|
|
18
|
-
Use ScratchpadRead to:
|
|
19
|
-
- Retrieve previously stored content and outputs
|
|
20
|
-
- Access detailed analysis or results from earlier steps
|
|
21
|
-
- Read messages or notes left by other agents
|
|
22
|
-
- Access cached computed data
|
|
23
|
-
- Retrieve content that was too long for direct responses
|
|
24
|
-
|
|
25
|
-
## Best Practices
|
|
26
|
-
|
|
27
|
-
- Any agent can read any scratchpad content
|
|
28
|
-
- Content is returned with line numbers for easy reference
|
|
29
|
-
- Use ScratchpadList first if you don't know what's stored
|
|
30
|
-
- Scratchpad data is temporary and lost when swarm ends
|
|
31
|
-
- For persistent data, use MemoryRead instead
|
|
32
|
-
|
|
33
|
-
## Examples
|
|
34
|
-
|
|
35
|
-
- Read status: file_path='status'
|
|
36
|
-
- Read analysis: file_path='api_analysis'
|
|
37
|
-
- Read agent notes: file_path='notes/backend'
|
|
38
|
-
DESC
|
|
39
|
-
|
|
40
|
-
param :file_path,
|
|
41
|
-
desc: "Path to read from scratchpad (e.g., 'status', 'result', 'notes/agent_x')",
|
|
42
|
-
required: true
|
|
43
|
-
|
|
44
|
-
class << self
|
|
45
|
-
# Create a ScratchpadRead tool for a specific scratchpad storage instance
|
|
46
|
-
#
|
|
47
|
-
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
48
|
-
# @return [ScratchpadRead] Tool instance
|
|
49
|
-
def create_for_scratchpad(scratchpad_storage)
|
|
50
|
-
new(scratchpad_storage)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Initialize with scratchpad storage instance
|
|
55
|
-
#
|
|
56
|
-
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
57
|
-
def initialize(scratchpad_storage)
|
|
58
|
-
super() # Call RubyLLM::Tool's initialize
|
|
59
|
-
@scratchpad_storage = scratchpad_storage
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Execute the tool
|
|
63
|
-
#
|
|
64
|
-
# @param file_path [String] Path to read from
|
|
65
|
-
# @return [String] Content at the path with line numbers, or error message
|
|
66
|
-
def execute(file_path:)
|
|
67
|
-
content = scratchpad_storage.read(file_path: file_path)
|
|
68
|
-
format_with_line_numbers(content)
|
|
69
|
-
rescue ArgumentError => e
|
|
70
|
-
validation_error(e.message)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
attr_reader :scratchpad_storage
|
|
76
|
-
|
|
77
|
-
def validation_error(message)
|
|
78
|
-
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Format content with line numbers (same format as Read tool)
|
|
82
|
-
#
|
|
83
|
-
# @param content [String] Content to format
|
|
84
|
-
# @return [String] Content with line numbers
|
|
85
|
-
def format_with_line_numbers(content)
|
|
86
|
-
lines = content.lines
|
|
87
|
-
output_lines = lines.each_with_index.map do |line, idx|
|
|
88
|
-
line_number = idx + 1
|
|
89
|
-
display_line = line.chomp
|
|
90
|
-
"#{line_number.to_s.rjust(6)}→#{display_line}"
|
|
91
|
-
end
|
|
92
|
-
output_lines.join("\n")
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|