claude_swarm 1.0.0 → 1.0.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/.claude/commands/release.md +1 -1
- data/.claude/hooks/lint-code-files.rb +65 -0
- data/.rubocop.yml +22 -2
- data/CHANGELOG.md +21 -1
- data/CLAUDE.md +1 -1
- data/CONTRIBUTING.md +69 -0
- data/README.md +27 -2
- data/Rakefile +71 -3
- data/analyze_coverage.rb +94 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
- data/docs/v2/README.md +308 -0
- data/docs/v2/guides/claude-code-agents.md +262 -0
- data/docs/v2/guides/complete-tutorial.md +3088 -0
- data/docs/v2/guides/getting-started.md +1456 -0
- data/docs/v2/guides/memory-adapters.md +998 -0
- data/docs/v2/guides/plugins.md +816 -0
- data/docs/v2/guides/quick-start-cli.md +1745 -0
- data/docs/v2/guides/rails-integration.md +1902 -0
- data/docs/v2/guides/swarm-memory.md +599 -0
- data/docs/v2/reference/cli.md +729 -0
- data/docs/v2/reference/ruby-dsl.md +2154 -0
- data/docs/v2/reference/yaml.md +1835 -0
- data/docs-team-swarm.yml +2222 -0
- data/examples/learning-assistant/assistant.md +7 -0
- data/examples/learning-assistant/example-memories/concept-example.md +90 -0
- data/examples/learning-assistant/example-memories/experience-example.md +66 -0
- data/examples/learning-assistant/example-memories/fact-example.md +76 -0
- data/examples/learning-assistant/example-memories/memory-index.md +78 -0
- data/examples/learning-assistant/example-memories/skill-example.md +168 -0
- data/examples/learning-assistant/learning_assistant.rb +34 -0
- data/examples/learning-assistant/learning_assistant.yml +20 -0
- data/examples/v2/dsl/01_basic.rb +44 -0
- data/examples/v2/dsl/02_core_parameters.rb +59 -0
- data/examples/v2/dsl/03_capabilities.rb +71 -0
- data/examples/v2/dsl/04_llm_parameters.rb +56 -0
- data/examples/v2/dsl/05_advanced_flags.rb +73 -0
- data/examples/v2/dsl/06_permissions.rb +80 -0
- data/examples/v2/dsl/07_mcp_server.rb +62 -0
- data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
- data/examples/v2/dsl/09_agent_hooks.rb +67 -0
- data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
- data/examples/v2/dsl/11_delegation.rb +60 -0
- data/examples/v2/dsl/12_complete_integration.rb +137 -0
- data/examples/v2/file_tools_swarm.yml +102 -0
- data/examples/v2/hooks/01_basic_hooks.rb +133 -0
- data/examples/v2/hooks/02_usage_tracking.rb +201 -0
- data/examples/v2/hooks/03_production_monitoring.rb +429 -0
- data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
- data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
- data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
- data/examples/v2/hooks/swarm_summary.sh +44 -0
- data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
- data/examples/v2/hooks/validate_bash.rb +59 -0
- data/examples/v2/multi_directory_permissions.yml +221 -0
- data/examples/v2/node_context_demo.rb +127 -0
- data/examples/v2/node_workflow.rb +173 -0
- data/examples/v2/path_resolution_demo.rb +216 -0
- data/examples/v2/simple-swarm-v2.rb +90 -0
- data/examples/v2/simple-swarm-v2.yml +62 -0
- data/examples/v2/swarm.yml +71 -0
- data/examples/v2/swarm_with_hooks.yml +61 -0
- data/examples/v2/swarm_with_hooks_simple.yml +25 -0
- data/examples/v2/think_tool_demo.rb +62 -0
- data/exe/swarm +6 -0
- data/lib/claude_swarm/claude_mcp_server.rb +0 -6
- data/lib/claude_swarm/cli.rb +10 -3
- data/lib/claude_swarm/commands/ps.rb +19 -20
- data/lib/claude_swarm/commands/show.rb +1 -1
- data/lib/claude_swarm/configuration.rb +10 -12
- data/lib/claude_swarm/mcp_generator.rb +10 -1
- data/lib/claude_swarm/orchestrator.rb +73 -49
- data/lib/claude_swarm/system_utils.rb +37 -11
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +1 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +7 -3
- data/lib/swarm_cli/cli.rb +201 -0
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
- data/lib/swarm_cli/commands/migrate.rb +55 -0
- data/lib/swarm_cli/commands/run.rb +173 -0
- data/lib/swarm_cli/config_loader.rb +97 -0
- data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
- data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
- data/lib/swarm_cli/interactive_repl.rb +918 -0
- data/lib/swarm_cli/mcp_serve_options.rb +44 -0
- data/lib/swarm_cli/mcp_tools_options.rb +59 -0
- data/lib/swarm_cli/migrate_options.rb +54 -0
- data/lib/swarm_cli/migrator.rb +132 -0
- data/lib/swarm_cli/options.rb +151 -0
- data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
- data/lib/swarm_cli/ui/components/content_block.rb +120 -0
- data/lib/swarm_cli/ui/components/divider.rb +57 -0
- data/lib/swarm_cli/ui/components/panel.rb +62 -0
- data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
- data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
- data/lib/swarm_cli/ui/formatters/number.rb +58 -0
- data/lib/swarm_cli/ui/formatters/text.rb +77 -0
- data/lib/swarm_cli/ui/formatters/time.rb +73 -0
- data/lib/swarm_cli/ui/icons.rb +59 -0
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
- data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
- data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
- data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
- data/lib/swarm_cli/version.rb +5 -0
- data/lib/swarm_cli.rb +44 -0
- data/lib/swarm_memory/adapters/base.rb +141 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
- data/lib/swarm_memory/chat_extension.rb +34 -0
- data/lib/swarm_memory/cli/commands.rb +306 -0
- data/lib/swarm_memory/core/entry.rb +37 -0
- data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
- data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
- data/lib/swarm_memory/core/path_normalizer.rb +75 -0
- data/lib/swarm_memory/core/semantic_index.rb +244 -0
- data/lib/swarm_memory/core/storage.rb +288 -0
- data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
- data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
- data/lib/swarm_memory/dsl/memory_config.rb +113 -0
- data/lib/swarm_memory/embeddings/embedder.rb +36 -0
- data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
- data/lib/swarm_memory/errors.rb +21 -0
- data/lib/swarm_memory/integration/cli_registration.rb +30 -0
- data/lib/swarm_memory/integration/configuration.rb +43 -0
- data/lib/swarm_memory/integration/registration.rb +31 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
- data/lib/swarm_memory/optimization/analyzer.rb +244 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
- data/lib/swarm_memory/prompts/memory.md.erb +109 -0
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +42 -0
- data/lib/swarm_memory/search/text_similarity.rb +80 -0
- data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
- data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
- data/lib/swarm_memory/tools/load_skill.rb +313 -0
- data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
- data/lib/swarm_memory/tools/memory_delete.rb +99 -0
- data/lib/swarm_memory/tools/memory_edit.rb +185 -0
- data/lib/swarm_memory/tools/memory_glob.rb +160 -0
- data/lib/swarm_memory/tools/memory_grep.rb +247 -0
- data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
- data/lib/swarm_memory/tools/memory_read.rb +123 -0
- data/lib/swarm_memory/tools/memory_write.rb +231 -0
- data/lib/swarm_memory/utils.rb +50 -0
- data/lib/swarm_memory/version.rb +5 -0
- data/lib/swarm_memory.rb +166 -0
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +461 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
- data/lib/swarm_sdk/agent/chat.rb +1159 -0
- data/lib/swarm_sdk/agent/context.rb +112 -0
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +556 -0
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +296 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +197 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +147 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +51 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +75 -0
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
- data/lib/swarm_sdk/swarm/builder.rb +586 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
- data/lib/swarm_sdk/swarm.rb +982 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/delegate.rb +164 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +228 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +93 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +157 -0
- data/llm.v2.txt +13407 -0
- data/rubocop/cop/security/no_reflection_methods.rb +47 -0
- data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
- data/swarm_cli.gemspec +57 -0
- data/swarm_memory.gemspec +28 -0
- data/swarm_sdk.gemspec +41 -0
- data/team.yml +1 -1
- data/team_full.yml +1875 -0
- data/{team_v2.yml → team_sdk.yml} +121 -52
- metadata +247 -4
- data/EXAMPLES.md +0 -164
|
@@ -0,0 +1,96 @@
|
|
|
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 < RubyLLM::Tool
|
|
11
|
+
define_method(:name) { "ScratchpadList" }
|
|
12
|
+
|
|
13
|
+
description <<~DESC
|
|
14
|
+
List all entries in scratchpad with their metadata.
|
|
15
|
+
Shows path, title, size, and last updated time for each entry.
|
|
16
|
+
Use this to discover what's stored in the scratchpad.
|
|
17
|
+
DESC
|
|
18
|
+
|
|
19
|
+
param :prefix,
|
|
20
|
+
desc: "Optional prefix to filter entries (e.g., 'notes/' to list all entries under notes/)",
|
|
21
|
+
required: false
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Create a ScratchpadList tool for a specific scratchpad storage instance
|
|
25
|
+
#
|
|
26
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
27
|
+
# @return [ScratchpadList] Tool instance
|
|
28
|
+
def create_for_scratchpad(scratchpad_storage)
|
|
29
|
+
new(scratchpad_storage)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Initialize with scratchpad storage instance
|
|
34
|
+
#
|
|
35
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
36
|
+
def initialize(scratchpad_storage)
|
|
37
|
+
super() # Call RubyLLM::Tool's initialize
|
|
38
|
+
@scratchpad_storage = scratchpad_storage
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Execute the tool
|
|
42
|
+
#
|
|
43
|
+
# @param prefix [String, nil] Optional prefix to filter entries
|
|
44
|
+
# @return [String] Formatted list of entries
|
|
45
|
+
def execute(prefix: nil)
|
|
46
|
+
entries = scratchpad_storage.list(prefix: prefix)
|
|
47
|
+
|
|
48
|
+
if entries.empty?
|
|
49
|
+
prefix_msg = prefix ? " with prefix '#{prefix}'" : ""
|
|
50
|
+
return "No entries found in scratchpad#{prefix_msg}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
result = []
|
|
54
|
+
prefix_msg = prefix ? " with prefix '#{prefix}'" : ""
|
|
55
|
+
result << "Scratchpad entries#{prefix_msg} (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
|
|
56
|
+
result << ""
|
|
57
|
+
|
|
58
|
+
entries.each do |entry|
|
|
59
|
+
time_str = entry[:updated_at].strftime("%Y-%m-%d %H:%M:%S")
|
|
60
|
+
result << " scratchpad://#{entry[:path]}"
|
|
61
|
+
result << " Title: #{entry[:title]}"
|
|
62
|
+
result << " Size: #{format_bytes(entry[:size])}"
|
|
63
|
+
result << " Updated: #{time_str}"
|
|
64
|
+
result << ""
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
result.join("\n").rstrip
|
|
68
|
+
rescue ArgumentError => e
|
|
69
|
+
validation_error(e.message)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
attr_reader :scratchpad_storage
|
|
75
|
+
|
|
76
|
+
def validation_error(message)
|
|
77
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Format bytes to human-readable size
|
|
81
|
+
#
|
|
82
|
+
# @param bytes [Integer] Number of bytes
|
|
83
|
+
# @return [String] Formatted size
|
|
84
|
+
def format_bytes(bytes)
|
|
85
|
+
if bytes >= 1_000_000
|
|
86
|
+
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
|
87
|
+
elsif bytes >= 1_000
|
|
88
|
+
"#{(bytes.to_f / 1_000).round(1)}KB"
|
|
89
|
+
else
|
|
90
|
+
"#{bytes}B"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
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 < RubyLLM::Tool
|
|
11
|
+
define_method(:name) { "ScratchpadRead" }
|
|
12
|
+
|
|
13
|
+
description <<~DESC
|
|
14
|
+
Read content from scratchpad.
|
|
15
|
+
Use this to retrieve temporary notes, results, or messages stored by any agent.
|
|
16
|
+
Any agent can read any scratchpad content.
|
|
17
|
+
DESC
|
|
18
|
+
|
|
19
|
+
param :file_path,
|
|
20
|
+
desc: "Path to read from scratchpad (e.g., 'status', 'result', 'notes/agent_x')",
|
|
21
|
+
required: true
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Create a ScratchpadRead tool for a specific scratchpad storage instance
|
|
25
|
+
#
|
|
26
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
27
|
+
# @return [ScratchpadRead] Tool instance
|
|
28
|
+
def create_for_scratchpad(scratchpad_storage)
|
|
29
|
+
new(scratchpad_storage)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Initialize with scratchpad storage instance
|
|
34
|
+
#
|
|
35
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
36
|
+
def initialize(scratchpad_storage)
|
|
37
|
+
super() # Call RubyLLM::Tool's initialize
|
|
38
|
+
@scratchpad_storage = scratchpad_storage
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Execute the tool
|
|
42
|
+
#
|
|
43
|
+
# @param file_path [String] Path to read from
|
|
44
|
+
# @return [String] Content at the path with line numbers, or error message
|
|
45
|
+
def execute(file_path:)
|
|
46
|
+
content = scratchpad_storage.read(file_path: file_path)
|
|
47
|
+
format_with_line_numbers(content)
|
|
48
|
+
rescue ArgumentError => e
|
|
49
|
+
validation_error(e.message)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
attr_reader :scratchpad_storage
|
|
55
|
+
|
|
56
|
+
def validation_error(message)
|
|
57
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Format content with line numbers (same format as Read tool)
|
|
61
|
+
#
|
|
62
|
+
# @param content [String] Content to format
|
|
63
|
+
# @return [String] Content with line numbers
|
|
64
|
+
def format_with_line_numbers(content)
|
|
65
|
+
lines = content.lines
|
|
66
|
+
output_lines = lines.each_with_index.map do |line, idx|
|
|
67
|
+
line_number = idx + 1
|
|
68
|
+
display_line = line.chomp
|
|
69
|
+
"#{line_number.to_s.rjust(6)}→#{display_line}"
|
|
70
|
+
end
|
|
71
|
+
output_lines.join("\n")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Tools
|
|
5
|
+
module Scratchpad
|
|
6
|
+
# Tool for writing content to scratchpad storage
|
|
7
|
+
#
|
|
8
|
+
# Stores content in volatile, shared storage for temporary communication.
|
|
9
|
+
# All agents in the swarm share the same scratchpad.
|
|
10
|
+
# Data is lost when the process ends (not persisted).
|
|
11
|
+
class ScratchpadWrite < RubyLLM::Tool
|
|
12
|
+
define_method(:name) { "ScratchpadWrite" }
|
|
13
|
+
|
|
14
|
+
description <<~DESC
|
|
15
|
+
Store content in scratchpad for temporary cross-agent communication.
|
|
16
|
+
Use this for quick notes, intermediate results, or coordination messages.
|
|
17
|
+
Any agent can read this content. Data is lost when the swarm ends.
|
|
18
|
+
|
|
19
|
+
For persistent storage that survives across sessions, use MemoryWrite instead.
|
|
20
|
+
|
|
21
|
+
Choose a simple, descriptive path. Examples: 'status', 'result', 'notes/agent_x'
|
|
22
|
+
DESC
|
|
23
|
+
|
|
24
|
+
param :file_path,
|
|
25
|
+
desc: "Simple path for the content (e.g., 'status', 'result', 'notes/agent_x')",
|
|
26
|
+
required: true
|
|
27
|
+
|
|
28
|
+
param :content,
|
|
29
|
+
desc: "Content to store in scratchpad (max 1MB per entry)",
|
|
30
|
+
required: true
|
|
31
|
+
|
|
32
|
+
param :title,
|
|
33
|
+
desc: "Brief title describing the content",
|
|
34
|
+
required: true
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# Create a ScratchpadWrite tool for a specific scratchpad storage instance
|
|
38
|
+
#
|
|
39
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
40
|
+
# @return [ScratchpadWrite] Tool instance
|
|
41
|
+
def create_for_scratchpad(scratchpad_storage)
|
|
42
|
+
new(scratchpad_storage)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Initialize with scratchpad storage instance
|
|
47
|
+
#
|
|
48
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
49
|
+
def initialize(scratchpad_storage)
|
|
50
|
+
super() # Call RubyLLM::Tool's initialize
|
|
51
|
+
@scratchpad_storage = scratchpad_storage
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Execute the tool
|
|
55
|
+
#
|
|
56
|
+
# @param file_path [String] Path to store content
|
|
57
|
+
# @param content [String] Content to store
|
|
58
|
+
# @param title [String] Brief title
|
|
59
|
+
# @return [String] Success message with path and size
|
|
60
|
+
def execute(file_path:, content:, title:)
|
|
61
|
+
entry = scratchpad_storage.write(file_path: file_path, content: content, title: title)
|
|
62
|
+
"Stored at scratchpad://#{file_path} (#{format_bytes(entry.size)})"
|
|
63
|
+
rescue ArgumentError => e
|
|
64
|
+
validation_error(e.message)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
attr_reader :scratchpad_storage
|
|
70
|
+
|
|
71
|
+
def validation_error(message)
|
|
72
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Format bytes to human-readable size
|
|
76
|
+
#
|
|
77
|
+
# @param bytes [Integer] Number of bytes
|
|
78
|
+
# @return [String] Formatted size
|
|
79
|
+
def format_bytes(bytes)
|
|
80
|
+
if bytes >= 1_000_000
|
|
81
|
+
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
|
82
|
+
elsif bytes >= 1_000
|
|
83
|
+
"#{(bytes.to_f / 1_000).round(1)}KB"
|
|
84
|
+
else
|
|
85
|
+
"#{bytes}B"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Tools
|
|
5
|
+
module Stores
|
|
6
|
+
# ReadTracker manages read-file tracking for all agents
|
|
7
|
+
#
|
|
8
|
+
# This module maintains a global registry of which files each agent has read
|
|
9
|
+
# during their conversation. This enables enforcement of the "read-before-write"
|
|
10
|
+
# and "read-before-edit" rules that ensure agents have context before modifying files.
|
|
11
|
+
#
|
|
12
|
+
# Each agent maintains an independent set of read files, keyed by agent identifier.
|
|
13
|
+
module ReadTracker
|
|
14
|
+
@read_files = {}
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
# Register that an agent has read a file
|
|
19
|
+
#
|
|
20
|
+
# @param agent_id [Symbol] The agent identifier
|
|
21
|
+
# @param file_path [String] The absolute path to the file
|
|
22
|
+
def register_read(agent_id, file_path)
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
@read_files[agent_id] ||= Set.new
|
|
25
|
+
@read_files[agent_id] << File.expand_path(file_path)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if an agent has read a file
|
|
30
|
+
#
|
|
31
|
+
# @param agent_id [Symbol] The agent identifier
|
|
32
|
+
# @param file_path [String] The absolute path to the file
|
|
33
|
+
# @return [Boolean] true if the agent has read this file
|
|
34
|
+
def file_read?(agent_id, file_path)
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
return false unless @read_files[agent_id]
|
|
37
|
+
|
|
38
|
+
@read_files[agent_id].include?(File.expand_path(file_path))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Clear read history for an agent (useful for testing)
|
|
43
|
+
#
|
|
44
|
+
# @param agent_id [Symbol] The agent identifier
|
|
45
|
+
def clear(agent_id)
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
@read_files.delete(agent_id)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Clear all read history (useful for testing)
|
|
52
|
+
def clear_all
|
|
53
|
+
@mutex.synchronize do
|
|
54
|
+
@read_files.clear
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Tools
|
|
5
|
+
module Stores
|
|
6
|
+
# ScratchpadStorage provides volatile, shared storage
|
|
7
|
+
#
|
|
8
|
+
# Features:
|
|
9
|
+
# - Shared: All agents share the same scratchpad
|
|
10
|
+
# - Volatile: NEVER persists - all data lost when process ends
|
|
11
|
+
# - Path-based: Hierarchical organization using file-path-like addresses
|
|
12
|
+
# - Metadata-rich: Stores content + title + timestamp + size
|
|
13
|
+
# - Thread-safe: Mutex-protected operations
|
|
14
|
+
#
|
|
15
|
+
# Use for temporary, cross-agent communication within a single session.
|
|
16
|
+
class ScratchpadStorage < Storage
|
|
17
|
+
# Initialize scratchpad storage (always volatile)
|
|
18
|
+
def initialize
|
|
19
|
+
super() # Initialize parent Storage class
|
|
20
|
+
@entries = {}
|
|
21
|
+
@total_size = 0
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Write content to scratchpad
|
|
26
|
+
#
|
|
27
|
+
# @param file_path [String] Path to store content
|
|
28
|
+
# @param content [String] Content to store
|
|
29
|
+
# @param title [String] Brief title describing the content
|
|
30
|
+
# @raise [ArgumentError] If size limits are exceeded
|
|
31
|
+
# @return [Entry] The created entry
|
|
32
|
+
def write(file_path:, content:, title:)
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
|
|
35
|
+
raise ArgumentError, "content is required" if content.nil?
|
|
36
|
+
raise ArgumentError, "title is required" if title.nil? || title.to_s.strip.empty?
|
|
37
|
+
|
|
38
|
+
content_size = content.bytesize
|
|
39
|
+
|
|
40
|
+
# Check entry size limit
|
|
41
|
+
if content_size > MAX_ENTRY_SIZE
|
|
42
|
+
raise ArgumentError, "Content exceeds maximum size (#{format_bytes(MAX_ENTRY_SIZE)}). " \
|
|
43
|
+
"Current: #{format_bytes(content_size)}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Calculate new total size
|
|
47
|
+
existing_entry = @entries[file_path]
|
|
48
|
+
existing_size = existing_entry ? existing_entry.size : 0
|
|
49
|
+
new_total_size = @total_size - existing_size + content_size
|
|
50
|
+
|
|
51
|
+
# Check total size limit
|
|
52
|
+
if new_total_size > MAX_TOTAL_SIZE
|
|
53
|
+
raise ArgumentError, "Scratchpad full (#{format_bytes(MAX_TOTAL_SIZE)} limit). " \
|
|
54
|
+
"Current: #{format_bytes(@total_size)}, " \
|
|
55
|
+
"Would be: #{format_bytes(new_total_size)}. " \
|
|
56
|
+
"Clear old entries or use smaller content."
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Create entry
|
|
60
|
+
entry = Entry.new(
|
|
61
|
+
content: content,
|
|
62
|
+
title: title,
|
|
63
|
+
updated_at: Time.now,
|
|
64
|
+
size: content_size,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Update storage
|
|
68
|
+
@entries[file_path] = entry
|
|
69
|
+
@total_size = new_total_size
|
|
70
|
+
|
|
71
|
+
entry
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Read content from scratchpad
|
|
76
|
+
#
|
|
77
|
+
# @param file_path [String] Path to read from
|
|
78
|
+
# @raise [ArgumentError] If path not found
|
|
79
|
+
# @return [String] Content at the path
|
|
80
|
+
def read(file_path:)
|
|
81
|
+
raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
|
|
82
|
+
|
|
83
|
+
entry = @entries[file_path]
|
|
84
|
+
raise ArgumentError, "scratchpad://#{file_path} not found" unless entry
|
|
85
|
+
|
|
86
|
+
entry.content
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Delete a specific entry
|
|
90
|
+
#
|
|
91
|
+
# @param file_path [String] Path to delete
|
|
92
|
+
# @raise [ArgumentError] If path not found
|
|
93
|
+
# @return [void]
|
|
94
|
+
def delete(file_path:)
|
|
95
|
+
@mutex.synchronize do
|
|
96
|
+
raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
|
|
97
|
+
|
|
98
|
+
entry = @entries[file_path]
|
|
99
|
+
raise ArgumentError, "scratchpad://#{file_path} not found" unless entry
|
|
100
|
+
|
|
101
|
+
# Update total size
|
|
102
|
+
@total_size -= entry.size
|
|
103
|
+
|
|
104
|
+
# Remove entry
|
|
105
|
+
@entries.delete(file_path)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# List scratchpad entries, optionally filtered by prefix
|
|
110
|
+
#
|
|
111
|
+
# @param prefix [String, nil] Filter by path prefix
|
|
112
|
+
# @return [Array<Hash>] Array of entry metadata (path, title, size, updated_at)
|
|
113
|
+
def list(prefix: nil)
|
|
114
|
+
entries = @entries
|
|
115
|
+
|
|
116
|
+
# Filter by prefix if provided
|
|
117
|
+
if prefix && !prefix.empty?
|
|
118
|
+
entries = entries.select { |path, _| path.start_with?(prefix) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Return metadata sorted by path
|
|
122
|
+
entries.map do |path, entry|
|
|
123
|
+
{
|
|
124
|
+
path: path,
|
|
125
|
+
title: entry.title,
|
|
126
|
+
size: entry.size,
|
|
127
|
+
updated_at: entry.updated_at,
|
|
128
|
+
}
|
|
129
|
+
end.sort_by { |e| e[:path] }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Search entries by glob pattern
|
|
133
|
+
#
|
|
134
|
+
# @param pattern [String] Glob pattern (e.g., "**/*.txt", "parallel/*/task_*")
|
|
135
|
+
# @return [Array<Hash>] Array of matching entry metadata, sorted by most recent first
|
|
136
|
+
def glob(pattern:)
|
|
137
|
+
raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
|
|
138
|
+
|
|
139
|
+
# Convert glob pattern to regex
|
|
140
|
+
regex = glob_to_regex(pattern)
|
|
141
|
+
|
|
142
|
+
# Filter entries by pattern
|
|
143
|
+
matching_entries = @entries.select { |path, _| regex.match?(path) }
|
|
144
|
+
|
|
145
|
+
# Return metadata sorted by most recent first
|
|
146
|
+
matching_entries.map do |path, entry|
|
|
147
|
+
{
|
|
148
|
+
path: path,
|
|
149
|
+
title: entry.title,
|
|
150
|
+
size: entry.size,
|
|
151
|
+
updated_at: entry.updated_at,
|
|
152
|
+
}
|
|
153
|
+
end.sort_by { |e| -e[:updated_at].to_f }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Search entry content by pattern
|
|
157
|
+
#
|
|
158
|
+
# @param pattern [String] Regular expression pattern to search for
|
|
159
|
+
# @param case_insensitive [Boolean] Whether to perform case-insensitive search
|
|
160
|
+
# @param output_mode [String] Output mode: "files_with_matches" (default), "content", or "count"
|
|
161
|
+
# @return [Array<Hash>, String] Results based on output_mode
|
|
162
|
+
def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
|
|
163
|
+
raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
|
|
164
|
+
|
|
165
|
+
# Create regex from pattern
|
|
166
|
+
flags = case_insensitive ? Regexp::IGNORECASE : 0
|
|
167
|
+
regex = Regexp.new(pattern, flags)
|
|
168
|
+
|
|
169
|
+
case output_mode
|
|
170
|
+
when "files_with_matches"
|
|
171
|
+
# Return just the paths that match
|
|
172
|
+
matching_paths = @entries.select { |_path, entry| regex.match?(entry.content) }
|
|
173
|
+
.map { |path, _| path }
|
|
174
|
+
.sort
|
|
175
|
+
matching_paths
|
|
176
|
+
when "content"
|
|
177
|
+
# Return paths with matching lines, sorted by most recent first
|
|
178
|
+
results = []
|
|
179
|
+
@entries.each do |path, entry|
|
|
180
|
+
matching_lines = []
|
|
181
|
+
entry.content.each_line.with_index(1) do |line, line_num|
|
|
182
|
+
matching_lines << { line_number: line_num, content: line.chomp } if regex.match?(line)
|
|
183
|
+
end
|
|
184
|
+
results << { path: path, matches: matching_lines, updated_at: entry.updated_at } unless matching_lines.empty?
|
|
185
|
+
end
|
|
186
|
+
results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
|
|
187
|
+
when "count"
|
|
188
|
+
# Return paths with match counts, sorted by most recent first
|
|
189
|
+
results = []
|
|
190
|
+
@entries.each do |path, entry|
|
|
191
|
+
count = entry.content.scan(regex).size
|
|
192
|
+
results << { path: path, count: count, updated_at: entry.updated_at } if count > 0
|
|
193
|
+
end
|
|
194
|
+
results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
|
|
195
|
+
else
|
|
196
|
+
raise ArgumentError, "Invalid output_mode: #{output_mode}. Must be 'files_with_matches', 'content', or 'count'"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Clear all entries
|
|
201
|
+
#
|
|
202
|
+
# @return [void]
|
|
203
|
+
def clear
|
|
204
|
+
@mutex.synchronize do
|
|
205
|
+
@entries.clear
|
|
206
|
+
@total_size = 0
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Get current total size
|
|
211
|
+
#
|
|
212
|
+
# @return [Integer] Total size in bytes
|
|
213
|
+
attr_reader :total_size
|
|
214
|
+
|
|
215
|
+
# Get number of entries
|
|
216
|
+
#
|
|
217
|
+
# @return [Integer] Number of entries
|
|
218
|
+
def size
|
|
219
|
+
@entries.size
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Tools
|
|
5
|
+
module Stores
|
|
6
|
+
# Abstract base class for hierarchical key-value storage with metadata
|
|
7
|
+
#
|
|
8
|
+
# Provides session-scoped storage for agents with path-based organization.
|
|
9
|
+
# Subclasses implement persistence behavior (volatile vs persistent).
|
|
10
|
+
#
|
|
11
|
+
# Features:
|
|
12
|
+
# - Path-based: Hierarchical organization using file-path-like addresses
|
|
13
|
+
# - Metadata-rich: Stores content + title + timestamp + size
|
|
14
|
+
# - Search capabilities: Glob patterns and grep-style content search
|
|
15
|
+
# - Thread-safe: Mutex-protected operations
|
|
16
|
+
class Storage
|
|
17
|
+
# Maximum size per entry (1MB)
|
|
18
|
+
MAX_ENTRY_SIZE = 1_000_000
|
|
19
|
+
|
|
20
|
+
# Maximum total storage size (100MB)
|
|
21
|
+
MAX_TOTAL_SIZE = 100_000_000
|
|
22
|
+
|
|
23
|
+
# Represents a single storage entry with metadata
|
|
24
|
+
Entry = Struct.new(:content, :title, :updated_at, :size, keyword_init: true)
|
|
25
|
+
|
|
26
|
+
# Initialize storage
|
|
27
|
+
#
|
|
28
|
+
# Subclasses should call super() in their initialize method.
|
|
29
|
+
# This base implementation does nothing - it exists only to satisfy RuboCop.
|
|
30
|
+
def initialize
|
|
31
|
+
# Base class initialization - subclasses implement their own logic
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Write content to storage
|
|
35
|
+
#
|
|
36
|
+
# @param file_path [String] Path to store content
|
|
37
|
+
# @param content [String] Content to store
|
|
38
|
+
# @param title [String] Brief title describing the content
|
|
39
|
+
# @raise [ArgumentError] If size limits are exceeded
|
|
40
|
+
# @return [Entry] The created entry
|
|
41
|
+
def write(file_path:, content:, title:)
|
|
42
|
+
raise NotImplementedError, "Subclass must implement #write"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Read content from storage
|
|
46
|
+
#
|
|
47
|
+
# @param file_path [String] Path to read from
|
|
48
|
+
# @raise [ArgumentError] If path not found
|
|
49
|
+
# @return [String] Content at the path
|
|
50
|
+
def read(file_path:)
|
|
51
|
+
raise NotImplementedError, "Subclass must implement #read"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Delete a specific entry
|
|
55
|
+
#
|
|
56
|
+
# @param file_path [String] Path to delete
|
|
57
|
+
# @raise [ArgumentError] If path not found
|
|
58
|
+
# @return [void]
|
|
59
|
+
def delete(file_path:)
|
|
60
|
+
raise NotImplementedError, "Subclass must implement #delete"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# List entries, optionally filtered by prefix
|
|
64
|
+
#
|
|
65
|
+
# @param prefix [String, nil] Filter by path prefix
|
|
66
|
+
# @return [Array<Hash>] Array of entry metadata (path, title, size, updated_at)
|
|
67
|
+
def list(prefix: nil)
|
|
68
|
+
raise NotImplementedError, "Subclass must implement #list"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Search entries by glob pattern
|
|
72
|
+
#
|
|
73
|
+
# @param pattern [String] Glob pattern (e.g., "**/*.txt", "parallel/*/task_*")
|
|
74
|
+
# @return [Array<Hash>] Array of matching entry metadata, sorted by most recent first
|
|
75
|
+
def glob(pattern:)
|
|
76
|
+
raise NotImplementedError, "Subclass must implement #glob"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Search entry content by pattern
|
|
80
|
+
#
|
|
81
|
+
# @param pattern [String] Regular expression pattern to search for
|
|
82
|
+
# @param case_insensitive [Boolean] Whether to perform case-insensitive search
|
|
83
|
+
# @param output_mode [String] Output mode: "files_with_matches" (default), "content", or "count"
|
|
84
|
+
# @return [Array<Hash>, String] Results based on output_mode
|
|
85
|
+
def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
|
|
86
|
+
raise NotImplementedError, "Subclass must implement #grep"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Clear all entries
|
|
90
|
+
#
|
|
91
|
+
# @return [void]
|
|
92
|
+
def clear
|
|
93
|
+
raise NotImplementedError, "Subclass must implement #clear"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Get current total size
|
|
97
|
+
#
|
|
98
|
+
# @return [Integer] Total size in bytes
|
|
99
|
+
def total_size
|
|
100
|
+
raise NotImplementedError, "Subclass must implement #total_size"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get number of entries
|
|
104
|
+
#
|
|
105
|
+
# @return [Integer] Number of entries
|
|
106
|
+
def size
|
|
107
|
+
raise NotImplementedError, "Subclass must implement #size"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
protected
|
|
111
|
+
|
|
112
|
+
# Format bytes to human-readable size
|
|
113
|
+
#
|
|
114
|
+
# @param bytes [Integer] Number of bytes
|
|
115
|
+
# @return [String] Formatted size (e.g., "1.5MB", "500.0KB")
|
|
116
|
+
def format_bytes(bytes)
|
|
117
|
+
if bytes >= 1_000_000
|
|
118
|
+
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
|
119
|
+
elsif bytes >= 1_000
|
|
120
|
+
"#{(bytes.to_f / 1_000).round(1)}KB"
|
|
121
|
+
else
|
|
122
|
+
"#{bytes}B"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Convert glob pattern to regex
|
|
127
|
+
#
|
|
128
|
+
# @param pattern [String] Glob pattern
|
|
129
|
+
# @return [Regexp] Regular expression
|
|
130
|
+
def glob_to_regex(pattern)
|
|
131
|
+
# Escape special regex characters except glob wildcards
|
|
132
|
+
escaped = Regexp.escape(pattern)
|
|
133
|
+
|
|
134
|
+
# Convert glob wildcards to regex
|
|
135
|
+
# ** matches any number of directories (including zero)
|
|
136
|
+
escaped = escaped.gsub('\*\*', ".*")
|
|
137
|
+
# * matches anything except directory separator
|
|
138
|
+
escaped = escaped.gsub('\*', "[^/]*")
|
|
139
|
+
# ? matches single character except directory separator
|
|
140
|
+
escaped = escaped.gsub('\?', "[^/]")
|
|
141
|
+
|
|
142
|
+
# Anchor to start and end
|
|
143
|
+
Regexp.new("\\A#{escaped}\\z")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|