claude_swarm 1.0.1 → 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 +14 -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 +6 -2
- 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,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for searching memory entries by glob pattern
|
|
6
|
+
#
|
|
7
|
+
# Finds memory entries matching a glob pattern (like filesystem glob).
|
|
8
|
+
# Each agent has its own isolated memory storage.
|
|
9
|
+
class MemoryGlob < RubyLLM::Tool
|
|
10
|
+
description <<~DESC
|
|
11
|
+
Search your memory entries using glob patterns (like filesystem glob).
|
|
12
|
+
|
|
13
|
+
REQUIRED: Provide the pattern parameter - the glob pattern to match entries against.
|
|
14
|
+
|
|
15
|
+
**Parameters:**
|
|
16
|
+
- pattern (REQUIRED): Glob pattern with wildcards (e.g., '**/*.txt', 'parallel/*/task_*', 'skill/**')
|
|
17
|
+
|
|
18
|
+
**Glob Pattern Syntax (Standard Ruby Glob):**
|
|
19
|
+
- `*` - matches .md files at a single directory level (e.g., 'fact/*' → fact/*.md)
|
|
20
|
+
- `**` - matches .md files recursively at any depth (e.g., 'fact/**' → fact/**/*.md)
|
|
21
|
+
- `?` - matches any single character (e.g., 'task_?')
|
|
22
|
+
- `[abc]` - matches any character in the set (e.g., 'task_[0-9]')
|
|
23
|
+
|
|
24
|
+
**Returns:**
|
|
25
|
+
List of matching .md memory entries with:
|
|
26
|
+
- Full memory:// path
|
|
27
|
+
- Entry title
|
|
28
|
+
- Size in bytes/KB/MB
|
|
29
|
+
|
|
30
|
+
**Note**: Only returns .md files (actual memory entries), not directory entries.
|
|
31
|
+
|
|
32
|
+
**MEMORY STRUCTURE (4 Fixed Categories Only):**
|
|
33
|
+
ALL patterns MUST target one of these 4 categories:
|
|
34
|
+
- concept/{domain}/** - Abstract ideas
|
|
35
|
+
- fact/{subfolder}/** - Concrete information
|
|
36
|
+
- skill/{domain}/** - Procedures
|
|
37
|
+
- experience/** - Lessons
|
|
38
|
+
INVALID: documentation/, reference/, parallel/, analysis/, tutorial/
|
|
39
|
+
|
|
40
|
+
**Common Use Cases:**
|
|
41
|
+
```
|
|
42
|
+
# Find direct .md files in fact/
|
|
43
|
+
MemoryGlob(pattern: "fact/*")
|
|
44
|
+
Result: fact/api.md (only direct children, not nested)
|
|
45
|
+
|
|
46
|
+
# Find ALL facts recursively
|
|
47
|
+
MemoryGlob(pattern: "fact/**")
|
|
48
|
+
Result: fact/api.md, fact/people/john.md, fact/people/jane.md, ...
|
|
49
|
+
|
|
50
|
+
# Find all skills recursively
|
|
51
|
+
MemoryGlob(pattern: "skill/**")
|
|
52
|
+
Result: skill/debugging/api-errors.md, skill/meta/deep-learning.md, ...
|
|
53
|
+
|
|
54
|
+
# Find all concepts in a domain
|
|
55
|
+
MemoryGlob(pattern: "concept/ruby/**")
|
|
56
|
+
Result: concept/ruby/classes.md, concept/ruby/modules.md, ...
|
|
57
|
+
|
|
58
|
+
# Find direct files in fact/people/
|
|
59
|
+
MemoryGlob(pattern: "fact/people/*")
|
|
60
|
+
Result: fact/people/john.md, fact/people/jane.md (not fact/people/teams/x.md)
|
|
61
|
+
|
|
62
|
+
# Find all experiences
|
|
63
|
+
MemoryGlob(pattern: "experience/**")
|
|
64
|
+
Result: experience/fixed-cors-bug.md, experience/optimization.md, ...
|
|
65
|
+
|
|
66
|
+
# Find debugging skills recursively
|
|
67
|
+
MemoryGlob(pattern: "skill/debugging/**")
|
|
68
|
+
Result: skill/debugging/api-errors.md, skill/debugging/performance.md, ...
|
|
69
|
+
|
|
70
|
+
# Find all entries (all categories)
|
|
71
|
+
MemoryGlob(pattern: "**/*")
|
|
72
|
+
Result: All .md entries across all 4 categories
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Understanding * vs **:**
|
|
76
|
+
- `fact/*` matches only direct .md files: fact/api.md
|
|
77
|
+
- `fact/**` matches ALL .md files recursively: fact/api.md, fact/people/john.md, ...
|
|
78
|
+
- To explore subdirectories, use recursive pattern and examine returned paths
|
|
79
|
+
|
|
80
|
+
**When to Use MemoryGlob:**
|
|
81
|
+
- Discovering what's in a memory hierarchy
|
|
82
|
+
- Finding all entries matching a naming convention
|
|
83
|
+
- Locating related entries by path pattern
|
|
84
|
+
- Exploring memory structure before reading specific entries
|
|
85
|
+
- Batch operations preparation (find all, then process each)
|
|
86
|
+
|
|
87
|
+
**Combining with Other Tools:**
|
|
88
|
+
1. Use MemoryGlob to find candidates
|
|
89
|
+
2. Use MemoryRead to examine specific entries
|
|
90
|
+
3. Use MemoryEdit/MemoryDelete to modify/remove them
|
|
91
|
+
|
|
92
|
+
**Tips:**
|
|
93
|
+
- Start with broad patterns and narrow down
|
|
94
|
+
- Use `**` for recursive searching entire hierarchies
|
|
95
|
+
- Combine with MemoryGrep if you need content-based search
|
|
96
|
+
- Check entry sizes to identify large entries
|
|
97
|
+
DESC
|
|
98
|
+
|
|
99
|
+
param :pattern,
|
|
100
|
+
desc: "Glob pattern - target concept/, fact/, skill/, or experience/ only (e.g., 'skill/**', 'concept/ruby/*', 'fact/people/*.md')",
|
|
101
|
+
required: true
|
|
102
|
+
|
|
103
|
+
# Initialize with storage instance
|
|
104
|
+
#
|
|
105
|
+
# @param storage [Core::Storage] Storage instance
|
|
106
|
+
def initialize(storage:)
|
|
107
|
+
super()
|
|
108
|
+
@storage = storage
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Override name to return simple "MemoryGlob"
|
|
112
|
+
def name
|
|
113
|
+
"MemoryGlob"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Execute the tool
|
|
117
|
+
#
|
|
118
|
+
# @param pattern [String] Glob pattern to match
|
|
119
|
+
# @return [String] Formatted list of matching entries
|
|
120
|
+
def execute(pattern:)
|
|
121
|
+
entries = @storage.glob(pattern: pattern)
|
|
122
|
+
|
|
123
|
+
if entries.empty?
|
|
124
|
+
return "No entries found matching pattern '#{pattern}'"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
result = []
|
|
128
|
+
result << "Memory entries matching '#{pattern}' (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
|
|
129
|
+
|
|
130
|
+
entries.each do |entry|
|
|
131
|
+
result << " memory://#{entry[:path]} - \"#{entry[:title]}\" (#{format_bytes(entry[:size])})"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
result.join("\n")
|
|
135
|
+
rescue ArgumentError => e
|
|
136
|
+
validation_error(e.message)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def validation_error(message)
|
|
142
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Format bytes to human-readable size
|
|
146
|
+
#
|
|
147
|
+
# @param bytes [Integer] Number of bytes
|
|
148
|
+
# @return [String] Formatted size
|
|
149
|
+
def format_bytes(bytes)
|
|
150
|
+
if bytes >= 1_000_000
|
|
151
|
+
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
|
152
|
+
elsif bytes >= 1_000
|
|
153
|
+
"#{(bytes.to_f / 1_000).round(1)}KB"
|
|
154
|
+
else
|
|
155
|
+
"#{bytes}B"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for searching memory content by pattern
|
|
6
|
+
#
|
|
7
|
+
# Searches content stored in memory entries using regex patterns.
|
|
8
|
+
# Each agent has its own isolated memory storage.
|
|
9
|
+
class MemoryGrep < RubyLLM::Tool
|
|
10
|
+
description <<~DESC
|
|
11
|
+
Search your memory content using regular expression patterns (like grep).
|
|
12
|
+
|
|
13
|
+
REQUIRED: Provide the pattern parameter - the regex pattern to search for in entry content.
|
|
14
|
+
|
|
15
|
+
MEMORY STRUCTURE: Searches across all 4 fixed categories (concept/, fact/, skill/, experience/)
|
|
16
|
+
NO OTHER top-level categories exist.
|
|
17
|
+
|
|
18
|
+
**Required Parameters:**
|
|
19
|
+
- pattern (REQUIRED): Regular expression pattern to search for (e.g., 'status: pending', 'TODO.*urgent', '\\btask_\\d+\\b')
|
|
20
|
+
|
|
21
|
+
**Optional Parameters:**
|
|
22
|
+
- path: Limit search to specific path (e.g., 'concept/', 'fact/api-design/', 'skill/ruby')
|
|
23
|
+
- case_insensitive: Set to true for case-insensitive search (default: false)
|
|
24
|
+
- output_mode: Choose output format - 'files_with_matches' (default), 'content', or 'count'
|
|
25
|
+
|
|
26
|
+
**Output Modes Explained:**
|
|
27
|
+
1. **files_with_matches** (default): Just shows which entries contain matches
|
|
28
|
+
- Fast and efficient for discovery
|
|
29
|
+
- Use when you want to know WHERE matches exist
|
|
30
|
+
|
|
31
|
+
2. **content**: Shows matching lines with line numbers
|
|
32
|
+
- See the actual matching content
|
|
33
|
+
- Use when you need to read the matches in context
|
|
34
|
+
|
|
35
|
+
3. **count**: Shows how many matches in each entry
|
|
36
|
+
- Quantify occurrences
|
|
37
|
+
- Use for statistics or finding entries with most matches
|
|
38
|
+
|
|
39
|
+
**Regular Expression Syntax:**
|
|
40
|
+
- Literal text: 'status: pending'
|
|
41
|
+
- Any character: 'task.done'
|
|
42
|
+
- Character classes: '[0-9]+' (digits), '[a-z]+' (lowercase)
|
|
43
|
+
- Word boundaries: '\\btodo\\b' (exact word)
|
|
44
|
+
- Anchors: '^Start' (line start), 'end$' (line end)
|
|
45
|
+
- Quantifiers: '*' (0+), '+' (1+), '?' (0 or 1), '{3}' (exactly 3)
|
|
46
|
+
- Alternation: 'pending|in-progress|blocked'
|
|
47
|
+
|
|
48
|
+
**Path Parameter - Directory-Style Filtering:**
|
|
49
|
+
The path parameter works just like searching in directories:
|
|
50
|
+
- 'concept/' - Search only concept entries
|
|
51
|
+
- 'fact/api-design' - Search only in fact/api-design (treats as directory)
|
|
52
|
+
- 'fact/api-design/' - Same as above
|
|
53
|
+
- 'skill/ruby/blocks.md' - Search only that specific file
|
|
54
|
+
|
|
55
|
+
**Examples:**
|
|
56
|
+
```
|
|
57
|
+
# Find entries containing "TODO" (case-sensitive)
|
|
58
|
+
MemoryGrep(pattern: "TODO")
|
|
59
|
+
|
|
60
|
+
# Search only in concepts
|
|
61
|
+
MemoryGrep(pattern: "TODO", path: "concept/")
|
|
62
|
+
|
|
63
|
+
# Search in a specific subdirectory
|
|
64
|
+
MemoryGrep(pattern: "endpoint", path: "fact/api-design")
|
|
65
|
+
|
|
66
|
+
# Search a specific file
|
|
67
|
+
MemoryGrep(pattern: "lambda", path: "skill/ruby/blocks.md")
|
|
68
|
+
|
|
69
|
+
# Find entries with any status (case-insensitive)
|
|
70
|
+
MemoryGrep(pattern: "status:", case_insensitive: true)
|
|
71
|
+
|
|
72
|
+
# Show actual content of matches in skills only
|
|
73
|
+
MemoryGrep(pattern: "error|warning|failed", path: "skill/", output_mode: "content")
|
|
74
|
+
|
|
75
|
+
# Count how many times "completed" appears in experiences
|
|
76
|
+
MemoryGrep(pattern: "completed", path: "experience/", output_mode: "count")
|
|
77
|
+
|
|
78
|
+
# Find task numbers in facts
|
|
79
|
+
MemoryGrep(pattern: "task_\\d+", path: "fact/")
|
|
80
|
+
|
|
81
|
+
# Find incomplete tasks
|
|
82
|
+
MemoryGrep(pattern: "^- \\[ \\]", output_mode: "content")
|
|
83
|
+
|
|
84
|
+
# Find entries mentioning specific functions
|
|
85
|
+
MemoryGrep(pattern: "\\bprocess_data\\(")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Use Cases:**
|
|
89
|
+
- Finding entries by keyword or phrase
|
|
90
|
+
- Locating TODO items or action items
|
|
91
|
+
- Searching for error messages or debugging info
|
|
92
|
+
- Finding entries about specific code/functions
|
|
93
|
+
- Identifying patterns in your memory
|
|
94
|
+
- Content-based discovery (vs MemoryGlob's path-based discovery)
|
|
95
|
+
|
|
96
|
+
**Combining with Other Tools:**
|
|
97
|
+
1. Use MemoryGrep to find entries containing specific content
|
|
98
|
+
2. Use MemoryRead to examine full entries
|
|
99
|
+
3. Use MemoryEdit to update the found content
|
|
100
|
+
|
|
101
|
+
**Tips:**
|
|
102
|
+
- Start with simple literal patterns before using complex regex
|
|
103
|
+
- Use case_insensitive=true for broader matches
|
|
104
|
+
- Use path parameter to limit search scope (faster and more precise)
|
|
105
|
+
- Use output_mode="content" to see context around matches
|
|
106
|
+
- Escape special regex characters with backslash: \\. \\* \\? \\[ \\]
|
|
107
|
+
- Test patterns on a small set before broad searches
|
|
108
|
+
- Use word boundaries (\\b) for exact word matching
|
|
109
|
+
DESC
|
|
110
|
+
|
|
111
|
+
param :pattern,
|
|
112
|
+
desc: "Regular expression pattern to search for",
|
|
113
|
+
required: true
|
|
114
|
+
|
|
115
|
+
param :path,
|
|
116
|
+
desc: "Limit search to specific path (e.g., 'concept/', 'fact/api-design/', 'skill/ruby/blocks.md')",
|
|
117
|
+
required: false
|
|
118
|
+
|
|
119
|
+
param :case_insensitive,
|
|
120
|
+
type: "boolean",
|
|
121
|
+
desc: "Set to true for case-insensitive search (default: false)",
|
|
122
|
+
required: false
|
|
123
|
+
|
|
124
|
+
param :output_mode,
|
|
125
|
+
desc: "Output mode: 'files_with_matches' (default), 'content', or 'count'",
|
|
126
|
+
required: false
|
|
127
|
+
|
|
128
|
+
# Initialize with storage instance
|
|
129
|
+
#
|
|
130
|
+
# @param storage [Core::Storage] Storage instance
|
|
131
|
+
def initialize(storage:)
|
|
132
|
+
super()
|
|
133
|
+
@storage = storage
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Override name to return simple "MemoryGrep"
|
|
137
|
+
def name
|
|
138
|
+
"MemoryGrep"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Execute the tool
|
|
142
|
+
#
|
|
143
|
+
# @param pattern [String] Regex pattern to search for
|
|
144
|
+
# @param path [String, nil] Optional path filter
|
|
145
|
+
# @param case_insensitive [Boolean] Whether to perform case-insensitive search
|
|
146
|
+
# @param output_mode [String] Output mode
|
|
147
|
+
# @return [String] Formatted search results
|
|
148
|
+
def execute(pattern:, path: nil, case_insensitive: false, output_mode: "files_with_matches")
|
|
149
|
+
results = @storage.grep(
|
|
150
|
+
pattern: pattern,
|
|
151
|
+
path: path,
|
|
152
|
+
case_insensitive: case_insensitive,
|
|
153
|
+
output_mode: output_mode,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
format_results(results, pattern, output_mode, path)
|
|
157
|
+
rescue ArgumentError => e
|
|
158
|
+
validation_error(e.message)
|
|
159
|
+
rescue RegexpError => e
|
|
160
|
+
validation_error("Invalid regex pattern: #{e.message}")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def validation_error(message)
|
|
166
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def format_results(results, pattern, output_mode, path_filter)
|
|
170
|
+
case output_mode
|
|
171
|
+
when "files_with_matches"
|
|
172
|
+
format_files_with_matches(results, pattern, path_filter)
|
|
173
|
+
when "content"
|
|
174
|
+
format_content(results, pattern, path_filter)
|
|
175
|
+
when "count"
|
|
176
|
+
format_count(results, pattern, path_filter)
|
|
177
|
+
else
|
|
178
|
+
validation_error("Invalid output_mode: #{output_mode}")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def format_search_header(pattern, path_filter)
|
|
183
|
+
if path_filter && !path_filter.empty?
|
|
184
|
+
"'#{pattern}' in #{path_filter}"
|
|
185
|
+
else
|
|
186
|
+
"'#{pattern}'"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def format_files_with_matches(paths, pattern, path_filter)
|
|
191
|
+
search_desc = format_search_header(pattern, path_filter)
|
|
192
|
+
|
|
193
|
+
if paths.empty?
|
|
194
|
+
return "No matches found for pattern #{search_desc}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
result = []
|
|
198
|
+
result << "Memory entries matching #{search_desc} (#{paths.size} #{paths.size == 1 ? "entry" : "entries"}):"
|
|
199
|
+
paths.each do |path|
|
|
200
|
+
result << " memory://#{path}"
|
|
201
|
+
end
|
|
202
|
+
result.join("\n")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def format_content(results, pattern, path_filter)
|
|
206
|
+
search_desc = format_search_header(pattern, path_filter)
|
|
207
|
+
|
|
208
|
+
if results.empty?
|
|
209
|
+
return "No matches found for pattern #{search_desc}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
total_matches = results.sum { |r| r[:matches].size }
|
|
213
|
+
output = []
|
|
214
|
+
output << "Memory entries matching #{search_desc} (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} #{total_matches == 1 ? "match" : "matches"}):"
|
|
215
|
+
output << ""
|
|
216
|
+
|
|
217
|
+
results.each do |result|
|
|
218
|
+
output << "memory://#{result[:path]}:"
|
|
219
|
+
result[:matches].each do |match|
|
|
220
|
+
output << " #{match[:line_number]}: #{match[:content]}"
|
|
221
|
+
end
|
|
222
|
+
output << ""
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
output.join("\n").rstrip
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def format_count(results, pattern, path_filter)
|
|
229
|
+
search_desc = format_search_header(pattern, path_filter)
|
|
230
|
+
|
|
231
|
+
if results.empty?
|
|
232
|
+
return "No matches found for pattern #{search_desc}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
total_matches = results.sum { |r| r[:count] }
|
|
236
|
+
output = []
|
|
237
|
+
output << "Memory entries matching #{search_desc} (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} total #{total_matches == 1 ? "match" : "matches"}):"
|
|
238
|
+
|
|
239
|
+
results.each do |result|
|
|
240
|
+
output << " memory://#{result[:path]}: #{result[:count]} #{result[:count] == 1 ? "match" : "matches"}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
output.join("\n")
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for performing multiple edits to a memory entry
|
|
6
|
+
#
|
|
7
|
+
# Applies multiple edit operations sequentially to a single memory entry.
|
|
8
|
+
# Each edit sees the result of all previous edits, allowing for
|
|
9
|
+
# coordinated multi-step transformations.
|
|
10
|
+
# Each agent has its own isolated memory storage.
|
|
11
|
+
class MemoryMultiEdit < RubyLLM::Tool
|
|
12
|
+
description <<~DESC
|
|
13
|
+
Perform multiple exact string replacements in a single memory entry (applies edits sequentially).
|
|
14
|
+
|
|
15
|
+
REQUIRED: Provide BOTH parameters - file_path and edits_json.
|
|
16
|
+
|
|
17
|
+
**Required Parameters:**
|
|
18
|
+
- file_path (REQUIRED): Path to memory entry - MUST start with concept/, fact/, skill/, or experience/
|
|
19
|
+
- edits_json (REQUIRED): JSON array of edit operations - each must have old_string, new_string, and optionally replace_all
|
|
20
|
+
|
|
21
|
+
**MEMORY STRUCTURE (4 Fixed Categories Only):**
|
|
22
|
+
- concept/{domain}/** - Abstract ideas
|
|
23
|
+
- fact/{subfolder}/** - Concrete information
|
|
24
|
+
- skill/{domain}/** - Procedures
|
|
25
|
+
- experience/** - Lessons
|
|
26
|
+
INVALID: documentation/, reference/, project/, code/, parallel/
|
|
27
|
+
|
|
28
|
+
**JSON Format:**
|
|
29
|
+
```json
|
|
30
|
+
[
|
|
31
|
+
{"old_string": "text to find", "new_string": "replacement text", "replace_all": false},
|
|
32
|
+
{"old_string": "another find", "new_string": "another replace", "replace_all": true}
|
|
33
|
+
]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**CRITICAL - Before Using This Tool:**
|
|
37
|
+
1. You MUST use MemoryRead on the entry first - edits without reading will FAIL
|
|
38
|
+
2. Copy text exactly from MemoryRead output, EXCLUDING the line number prefix
|
|
39
|
+
3. Line number format: " 123→actual content" - only use text AFTER the arrow
|
|
40
|
+
4. Edits are applied SEQUENTIALLY - later edits see results of earlier edits
|
|
41
|
+
5. If ANY edit fails, NO changes are saved (all-or-nothing)
|
|
42
|
+
|
|
43
|
+
**How Sequential Edits Work:**
|
|
44
|
+
```
|
|
45
|
+
Original: "status: pending, priority: low"
|
|
46
|
+
|
|
47
|
+
Edit 1: "pending" → "in-progress"
|
|
48
|
+
Result: "status: in-progress, priority: low"
|
|
49
|
+
|
|
50
|
+
Edit 2: "low" → "high" (sees Edit 1's result)
|
|
51
|
+
Final: "status: in-progress, priority: high"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Use Cases:**
|
|
55
|
+
- Making multiple coordinated changes in one operation
|
|
56
|
+
- Updating several related fields at once
|
|
57
|
+
- Chaining transformations where order matters
|
|
58
|
+
- Bulk find-and-replace operations
|
|
59
|
+
|
|
60
|
+
**Examples:**
|
|
61
|
+
```
|
|
62
|
+
# Update multiple fields in an experience
|
|
63
|
+
MemoryMultiEdit(
|
|
64
|
+
file_path: "experience/api-debugging.md",
|
|
65
|
+
edits_json: '[
|
|
66
|
+
{"old_string": "status: in-progress", "new_string": "status: resolved"},
|
|
67
|
+
{"old_string": "confidence: medium", "new_string": "confidence: high"}
|
|
68
|
+
]'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Rename function and update calls in a concept
|
|
72
|
+
MemoryMultiEdit(
|
|
73
|
+
file_path: "concept/ruby/functions.md",
|
|
74
|
+
edits_json: '[
|
|
75
|
+
{"old_string": "def old_func_name", "new_string": "def new_func_name"},
|
|
76
|
+
{"old_string": "old_func_name()", "new_string": "new_func_name()", "replace_all": true}
|
|
77
|
+
]'
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Important Notes:**
|
|
82
|
+
- All edits in the array must be valid JSON objects
|
|
83
|
+
- Each old_string must be different from its new_string
|
|
84
|
+
- Each old_string must be unique in content UNLESS replace_all is true
|
|
85
|
+
- Failed edit shows which previous edits succeeded
|
|
86
|
+
- More efficient than multiple MemoryEdit calls
|
|
87
|
+
DESC
|
|
88
|
+
|
|
89
|
+
param :file_path,
|
|
90
|
+
desc: "Path to memory entry - MUST start with concept/, fact/, skill/, or experience/ (e.g., 'experience/api-debugging.md', 'concept/ruby/functions.md')",
|
|
91
|
+
required: true
|
|
92
|
+
|
|
93
|
+
param :edits_json,
|
|
94
|
+
type: "string",
|
|
95
|
+
desc: <<~DESC.chomp,
|
|
96
|
+
JSON array of edit operations. Each edit must have:
|
|
97
|
+
old_string (exact text to replace),
|
|
98
|
+
new_string (replacement text),
|
|
99
|
+
and optionally replace_all (boolean, default false).
|
|
100
|
+
Example: [{"old_string":"foo","new_string":"bar","replace_all":false}]
|
|
101
|
+
DESC
|
|
102
|
+
required: true
|
|
103
|
+
|
|
104
|
+
# Initialize with storage instance and agent name
|
|
105
|
+
#
|
|
106
|
+
# @param storage [Core::Storage] Storage instance
|
|
107
|
+
# @param agent_name [String, Symbol] Agent identifier
|
|
108
|
+
def initialize(storage:, agent_name:)
|
|
109
|
+
super()
|
|
110
|
+
@storage = storage
|
|
111
|
+
@agent_name = agent_name.to_sym
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Override name to return simple "MemoryMultiEdit"
|
|
115
|
+
def name
|
|
116
|
+
"MemoryMultiEdit"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Execute the tool
|
|
120
|
+
#
|
|
121
|
+
# @param file_path [String] Path to memory entry
|
|
122
|
+
# @param edits_json [String] JSON array of edit operations
|
|
123
|
+
# @return [String] Success message or error
|
|
124
|
+
def execute(file_path:, edits_json:)
|
|
125
|
+
# Validate inputs
|
|
126
|
+
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
|
127
|
+
|
|
128
|
+
# Parse JSON
|
|
129
|
+
edits = begin
|
|
130
|
+
JSON.parse(edits_json)
|
|
131
|
+
rescue JSON::ParserError
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
return validation_error("Invalid JSON format. Please provide a valid JSON array of edit operations.") if edits.nil?
|
|
136
|
+
|
|
137
|
+
return validation_error("edits must be an array") unless edits.is_a?(Array)
|
|
138
|
+
return validation_error("edits array cannot be empty") if edits.empty?
|
|
139
|
+
|
|
140
|
+
# Read current content (this will raise ArgumentError if entry doesn't exist)
|
|
141
|
+
content = @storage.read(file_path: file_path)
|
|
142
|
+
|
|
143
|
+
# Enforce read-before-edit
|
|
144
|
+
unless Core::StorageReadTracker.entry_read?(@agent_name, file_path)
|
|
145
|
+
return validation_error(
|
|
146
|
+
"Cannot edit memory entry without reading it first. " \
|
|
147
|
+
"You must use MemoryRead on 'memory://#{file_path}' before editing it. " \
|
|
148
|
+
"This ensures you have the current content to match against.",
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Validate edit operations
|
|
153
|
+
validated_edits = []
|
|
154
|
+
edits.each_with_index do |edit, index|
|
|
155
|
+
unless edit.is_a?(Hash)
|
|
156
|
+
return validation_error("Edit at index #{index} must be a hash/object with old_string and new_string")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Convert string keys to symbols for consistency
|
|
160
|
+
edit = edit.transform_keys(&:to_sym)
|
|
161
|
+
|
|
162
|
+
unless edit[:old_string]
|
|
163
|
+
return validation_error("Edit at index #{index} missing required field 'old_string'")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
unless edit[:new_string]
|
|
167
|
+
return validation_error("Edit at index #{index} missing required field 'new_string'")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# old_string and new_string must be different
|
|
171
|
+
if edit[:old_string] == edit[:new_string]
|
|
172
|
+
return validation_error("Edit at index #{index}: old_string and new_string must be different")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
validated_edits << {
|
|
176
|
+
old_string: edit[:old_string].to_s,
|
|
177
|
+
new_string: edit[:new_string].to_s,
|
|
178
|
+
replace_all: edit[:replace_all] == true,
|
|
179
|
+
index: index,
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Apply edits sequentially
|
|
184
|
+
results = []
|
|
185
|
+
current_content = content
|
|
186
|
+
|
|
187
|
+
validated_edits.each do |edit|
|
|
188
|
+
# Check if old_string exists in current content
|
|
189
|
+
unless current_content.include?(edit[:old_string])
|
|
190
|
+
return error_with_results(
|
|
191
|
+
<<~ERROR.chomp,
|
|
192
|
+
Edit #{edit[:index]}: old_string not found in memory entry.
|
|
193
|
+
Make sure it matches exactly, including all whitespace and indentation.
|
|
194
|
+
Do not include line number prefixes from MemoryRead tool output.
|
|
195
|
+
Note: This edit follows #{edit[:index]} previous edit(s) which may have changed the content.
|
|
196
|
+
ERROR
|
|
197
|
+
results,
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Count occurrences
|
|
202
|
+
occurrences = current_content.scan(edit[:old_string]).count
|
|
203
|
+
|
|
204
|
+
# If not replace_all and multiple occurrences, error
|
|
205
|
+
if !edit[:replace_all] && occurrences > 1
|
|
206
|
+
return error_with_results(
|
|
207
|
+
<<~ERROR.chomp,
|
|
208
|
+
Edit #{edit[:index]}: Found #{occurrences} occurrences of old_string.
|
|
209
|
+
Either provide more surrounding context to make the match unique, or set replace_all: true to replace all occurrences.
|
|
210
|
+
ERROR
|
|
211
|
+
results,
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Perform replacement
|
|
216
|
+
new_content = if edit[:replace_all]
|
|
217
|
+
current_content.gsub(edit[:old_string], edit[:new_string])
|
|
218
|
+
else
|
|
219
|
+
current_content.sub(edit[:old_string], edit[:new_string])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Record result
|
|
223
|
+
replaced_count = edit[:replace_all] ? occurrences : 1
|
|
224
|
+
results << {
|
|
225
|
+
index: edit[:index],
|
|
226
|
+
status: "success",
|
|
227
|
+
occurrences: replaced_count,
|
|
228
|
+
message: "Replaced #{replaced_count} occurrence(s)",
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Update content for next edit
|
|
232
|
+
current_content = new_content
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Get existing entry
|
|
236
|
+
entry = @storage.read_entry(file_path: file_path)
|
|
237
|
+
|
|
238
|
+
# Write updated content back (preserving the title)
|
|
239
|
+
@storage.write(
|
|
240
|
+
file_path: file_path,
|
|
241
|
+
content: current_content,
|
|
242
|
+
title: entry.title,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Build success message
|
|
246
|
+
total_replacements = results.sum { |r| r[:occurrences] }
|
|
247
|
+
message = "Successfully applied #{validated_edits.size} edit(s) to memory://#{file_path}\n"
|
|
248
|
+
message += "Total replacements: #{total_replacements}\n\n"
|
|
249
|
+
message += "Details:\n"
|
|
250
|
+
results.each do |result|
|
|
251
|
+
message += " Edit #{result[:index]}: #{result[:message]}\n"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
message
|
|
255
|
+
rescue ArgumentError => e
|
|
256
|
+
validation_error(e.message)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
private
|
|
260
|
+
|
|
261
|
+
def validation_error(message)
|
|
262
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def error_with_results(message, results)
|
|
266
|
+
output = "<tool_use_error>InputValidationError: #{message}\n\n"
|
|
267
|
+
|
|
268
|
+
if results.any?
|
|
269
|
+
output += "Previous successful edits before error:\n"
|
|
270
|
+
results.each do |result|
|
|
271
|
+
output += " Edit #{result[:index]}: #{result[:message]}\n"
|
|
272
|
+
end
|
|
273
|
+
output += "\n"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
output += "Note: The memory entry has NOT been modified. All or nothing approach - if any edit fails, no changes are saved.</tool_use_error>"
|
|
277
|
+
output
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|