swarm_memory 2.0.0

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.
Files changed (189) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/lib/claude_swarm/base_executor.rb +133 -0
  4. data/lib/claude_swarm/claude_code_executor.rb +349 -0
  5. data/lib/claude_swarm/claude_mcp_server.rb +77 -0
  6. data/lib/claude_swarm/cli.rb +712 -0
  7. data/lib/claude_swarm/commands/ps.rb +216 -0
  8. data/lib/claude_swarm/commands/show.rb +139 -0
  9. data/lib/claude_swarm/configuration.rb +363 -0
  10. data/lib/claude_swarm/hooks/session_start_hook.rb +42 -0
  11. data/lib/claude_swarm/json_handler.rb +91 -0
  12. data/lib/claude_swarm/mcp_generator.rb +248 -0
  13. data/lib/claude_swarm/openai/chat_completion.rb +264 -0
  14. data/lib/claude_swarm/openai/executor.rb +254 -0
  15. data/lib/claude_swarm/openai/responses.rb +338 -0
  16. data/lib/claude_swarm/orchestrator.rb +879 -0
  17. data/lib/claude_swarm/process_tracker.rb +78 -0
  18. data/lib/claude_swarm/session_cost_calculator.rb +209 -0
  19. data/lib/claude_swarm/session_path.rb +42 -0
  20. data/lib/claude_swarm/settings_generator.rb +77 -0
  21. data/lib/claude_swarm/system_utils.rb +46 -0
  22. data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
  23. data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
  24. data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
  25. data/lib/claude_swarm/tools/task_tool.rb +63 -0
  26. data/lib/claude_swarm/version.rb +5 -0
  27. data/lib/claude_swarm/worktree_manager.rb +475 -0
  28. data/lib/claude_swarm/yaml_loader.rb +22 -0
  29. data/lib/claude_swarm.rb +69 -0
  30. data/lib/swarm_cli/cli.rb +201 -0
  31. data/lib/swarm_cli/command_registry.rb +61 -0
  32. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  33. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  34. data/lib/swarm_cli/commands/migrate.rb +55 -0
  35. data/lib/swarm_cli/commands/run.rb +173 -0
  36. data/lib/swarm_cli/config_loader.rb +97 -0
  37. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  38. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  39. data/lib/swarm_cli/interactive_repl.rb +918 -0
  40. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  41. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  42. data/lib/swarm_cli/migrate_options.rb +54 -0
  43. data/lib/swarm_cli/migrator.rb +132 -0
  44. data/lib/swarm_cli/options.rb +151 -0
  45. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  46. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  47. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  48. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  49. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  50. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  51. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  52. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  53. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  54. data/lib/swarm_cli/ui/icons.rb +59 -0
  55. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  56. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  57. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  58. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  59. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  60. data/lib/swarm_cli/version.rb +5 -0
  61. data/lib/swarm_cli.rb +45 -0
  62. data/lib/swarm_memory/adapters/base.rb +140 -0
  63. data/lib/swarm_memory/adapters/filesystem_adapter.rb +789 -0
  64. data/lib/swarm_memory/chat_extension.rb +34 -0
  65. data/lib/swarm_memory/cli/commands.rb +306 -0
  66. data/lib/swarm_memory/core/entry.rb +37 -0
  67. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  68. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  69. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  70. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  71. data/lib/swarm_memory/core/storage.rb +286 -0
  72. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  73. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  74. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  75. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  76. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  77. data/lib/swarm_memory/errors.rb +21 -0
  78. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  79. data/lib/swarm_memory/integration/configuration.rb +43 -0
  80. data/lib/swarm_memory/integration/registration.rb +31 -0
  81. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  82. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  83. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  84. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  85. data/lib/swarm_memory/prompts/memory_assistant.md.erb +139 -0
  86. data/lib/swarm_memory/prompts/memory_researcher.md.erb +201 -0
  87. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +76 -0
  88. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  89. data/lib/swarm_memory/search/text_search.rb +40 -0
  90. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  91. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  92. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  93. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  94. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  95. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  96. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  97. data/lib/swarm_memory/tools/memory_glob.rb +145 -0
  98. data/lib/swarm_memory/tools/memory_grep.rb +209 -0
  99. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  100. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  101. data/lib/swarm_memory/tools/memory_write.rb +215 -0
  102. data/lib/swarm_memory/utils.rb +50 -0
  103. data/lib/swarm_memory/version.rb +5 -0
  104. data/lib/swarm_memory.rb +166 -0
  105. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  106. data/lib/swarm_sdk/agent/builder.rb +461 -0
  107. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  108. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  109. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  110. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  111. data/lib/swarm_sdk/agent/chat.rb +1144 -0
  112. data/lib/swarm_sdk/agent/context.rb +112 -0
  113. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  114. data/lib/swarm_sdk/agent/definition.rb +556 -0
  115. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  116. data/lib/swarm_sdk/configuration.rb +296 -0
  117. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  118. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  119. data/lib/swarm_sdk/context_compactor.rb +340 -0
  120. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  121. data/lib/swarm_sdk/hooks/context.rb +197 -0
  122. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  123. data/lib/swarm_sdk/hooks/error.rb +29 -0
  124. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  125. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  126. data/lib/swarm_sdk/hooks/result.rb +150 -0
  127. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  128. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  129. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  130. data/lib/swarm_sdk/log_collector.rb +51 -0
  131. data/lib/swarm_sdk/log_stream.rb +69 -0
  132. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  133. data/lib/swarm_sdk/model_aliases.json +5 -0
  134. data/lib/swarm_sdk/models.json +1 -0
  135. data/lib/swarm_sdk/models.rb +120 -0
  136. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  137. data/lib/swarm_sdk/node/builder.rb +439 -0
  138. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  139. data/lib/swarm_sdk/node_context.rb +170 -0
  140. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  141. data/lib/swarm_sdk/permissions/config.rb +239 -0
  142. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  143. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  144. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  145. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  146. data/lib/swarm_sdk/plugin.rb +147 -0
  147. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  148. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  149. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  150. data/lib/swarm_sdk/result.rb +97 -0
  151. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  152. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  153. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  154. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  155. data/lib/swarm_sdk/swarm/tool_configurator.rb +416 -0
  156. data/lib/swarm_sdk/swarm.rb +982 -0
  157. data/lib/swarm_sdk/tools/bash.rb +274 -0
  158. data/lib/swarm_sdk/tools/clock.rb +44 -0
  159. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  160. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  161. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  162. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  163. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  164. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  165. data/lib/swarm_sdk/tools/edit.rb +150 -0
  166. data/lib/swarm_sdk/tools/glob.rb +158 -0
  167. data/lib/swarm_sdk/tools/grep.rb +228 -0
  168. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  169. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  170. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  171. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  172. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  173. data/lib/swarm_sdk/tools/read.rb +251 -0
  174. data/lib/swarm_sdk/tools/registry.rb +93 -0
  175. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  176. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  177. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  178. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  179. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  180. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  181. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  182. data/lib/swarm_sdk/tools/think.rb +95 -0
  183. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  184. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  185. data/lib/swarm_sdk/tools/write.rb +117 -0
  186. data/lib/swarm_sdk/utils.rb +50 -0
  187. data/lib/swarm_sdk/version.rb +5 -0
  188. data/lib/swarm_sdk.rb +167 -0
  189. metadata +313 -0
@@ -0,0 +1,145 @@
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:**
19
+ - `*` - matches any characters within a single directory level (e.g., 'analysis/*')
20
+ - `**` - matches any characters across multiple directory levels recursively (e.g., 'parallel/**')
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 entries with:
26
+ - Full memory:// path
27
+ - Entry title
28
+ - Size in bytes/KB/MB
29
+
30
+ **MEMORY STRUCTURE (4 Fixed Categories Only):**
31
+ ALL patterns MUST target one of these 4 categories:
32
+ - concept/{domain}/** - Abstract ideas
33
+ - fact/{subfolder}/** - Concrete information
34
+ - skill/{domain}/** - Procedures
35
+ - experience/** - Lessons
36
+ INVALID: documentation/, reference/, parallel/, analysis/, tutorial/
37
+
38
+ **Common Use Cases:**
39
+ ```
40
+ # Find all skills
41
+ MemoryGlob(pattern: "skill/**")
42
+ Result: skill/debugging/api-errors.md, skill/meta/deep-learning.md, ...
43
+
44
+ # Find all concepts in a domain
45
+ MemoryGlob(pattern: "concept/ruby/**")
46
+ Result: concept/ruby/classes.md, concept/ruby/modules.md, ...
47
+
48
+ # Find all facts about people
49
+ MemoryGlob(pattern: "fact/people/*")
50
+ Result: fact/people/john.md, fact/people/jane.md, ...
51
+
52
+ # Find all experiences
53
+ MemoryGlob(pattern: "experience/**")
54
+ Result: experience/fixed-cors-bug.md, experience/optimization.md, ...
55
+
56
+ # Find debugging skills
57
+ MemoryGlob(pattern: "skill/debugging/*")
58
+ Result: skill/debugging/api-errors.md, skill/debugging/performance.md, ...
59
+
60
+ # Find all entries (all categories)
61
+ MemoryGlob(pattern: "**/*")
62
+ Result: All entries across all 4 categories
63
+ ```
64
+
65
+ **When to Use MemoryGlob:**
66
+ - Discovering what's in a memory hierarchy
67
+ - Finding all entries matching a naming convention
68
+ - Locating related entries by path pattern
69
+ - Exploring memory structure before reading specific entries
70
+ - Batch operations preparation (find all, then process each)
71
+
72
+ **Combining with Other Tools:**
73
+ 1. Use MemoryGlob to find candidates
74
+ 2. Use MemoryRead to examine specific entries
75
+ 3. Use MemoryEdit/MemoryDelete to modify/remove them
76
+
77
+ **Tips:**
78
+ - Start with broad patterns and narrow down
79
+ - Use `**` for recursive searching entire hierarchies
80
+ - Combine with MemoryGrep if you need content-based search
81
+ - Check entry sizes to identify large entries
82
+ DESC
83
+
84
+ param :pattern,
85
+ desc: "Glob pattern - target concept/, fact/, skill/, or experience/ only (e.g., 'skill/**', 'concept/ruby/*', 'fact/people/*.md')",
86
+ required: true
87
+
88
+ # Initialize with storage instance
89
+ #
90
+ # @param storage [Core::Storage] Storage instance
91
+ def initialize(storage:)
92
+ super()
93
+ @storage = storage
94
+ end
95
+
96
+ # Override name to return simple "MemoryGlob"
97
+ def name
98
+ "MemoryGlob"
99
+ end
100
+
101
+ # Execute the tool
102
+ #
103
+ # @param pattern [String] Glob pattern to match
104
+ # @return [String] Formatted list of matching entries
105
+ def execute(pattern:)
106
+ entries = @storage.glob(pattern: pattern)
107
+
108
+ if entries.empty?
109
+ return "No entries found matching pattern '#{pattern}'"
110
+ end
111
+
112
+ result = []
113
+ result << "Memory entries matching '#{pattern}' (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
114
+
115
+ entries.each do |entry|
116
+ result << " memory://#{entry[:path]} - \"#{entry[:title]}\" (#{format_bytes(entry[:size])})"
117
+ end
118
+
119
+ result.join("\n")
120
+ rescue ArgumentError => e
121
+ validation_error(e.message)
122
+ end
123
+
124
+ private
125
+
126
+ def validation_error(message)
127
+ "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
128
+ end
129
+
130
+ # Format bytes to human-readable size
131
+ #
132
+ # @param bytes [Integer] Number of bytes
133
+ # @return [String] Formatted size
134
+ def format_bytes(bytes)
135
+ if bytes >= 1_000_000
136
+ "#{(bytes.to_f / 1_000_000).round(1)}MB"
137
+ elsif bytes >= 1_000
138
+ "#{(bytes.to_f / 1_000).round(1)}KB"
139
+ else
140
+ "#{bytes}B"
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,209 @@
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
+ - case_insensitive: Set to true for case-insensitive search (default: false)
23
+ - output_mode: Choose output format - 'files_with_matches' (default), 'content', or 'count'
24
+
25
+ **Output Modes Explained:**
26
+ 1. **files_with_matches** (default): Just shows which entries contain matches
27
+ - Fast and efficient for discovery
28
+ - Use when you want to know WHERE matches exist
29
+
30
+ 2. **content**: Shows matching lines with line numbers
31
+ - See the actual matching content
32
+ - Use when you need to read the matches in context
33
+
34
+ 3. **count**: Shows how many matches in each entry
35
+ - Quantify occurrences
36
+ - Use for statistics or finding entries with most matches
37
+
38
+ **Regular Expression Syntax:**
39
+ - Literal text: 'status: pending'
40
+ - Any character: 'task.done'
41
+ - Character classes: '[0-9]+' (digits), '[a-z]+' (lowercase)
42
+ - Word boundaries: '\\btodo\\b' (exact word)
43
+ - Anchors: '^Start' (line start), 'end$' (line end)
44
+ - Quantifiers: '*' (0+), '+' (1+), '?' (0 or 1), '{3}' (exactly 3)
45
+ - Alternation: 'pending|in-progress|blocked'
46
+
47
+ **Examples:**
48
+ ```
49
+ # Find entries containing "TODO" (case-sensitive)
50
+ MemoryGrep(pattern: "TODO")
51
+
52
+ # Find entries with any status (case-insensitive)
53
+ MemoryGrep(pattern: "status:", case_insensitive: true)
54
+
55
+ # Show actual content of matches
56
+ MemoryGrep(pattern: "error|warning|failed", output_mode: "content")
57
+
58
+ # Count how many times "completed" appears in each entry
59
+ MemoryGrep(pattern: "completed", output_mode: "count")
60
+
61
+ # Find task numbers
62
+ MemoryGrep(pattern: "task_\\d+")
63
+
64
+ # Find incomplete tasks
65
+ MemoryGrep(pattern: "^- \\[ \\]", output_mode: "content")
66
+
67
+ # Find entries mentioning specific functions
68
+ MemoryGrep(pattern: "\\bprocess_data\\(")
69
+ ```
70
+
71
+ **Use Cases:**
72
+ - Finding entries by keyword or phrase
73
+ - Locating TODO items or action items
74
+ - Searching for error messages or debugging info
75
+ - Finding entries about specific code/functions
76
+ - Identifying patterns in your memory
77
+ - Content-based discovery (vs MemoryGlob's path-based discovery)
78
+
79
+ **Combining with Other Tools:**
80
+ 1. Use MemoryGrep to find entries containing specific content
81
+ 2. Use MemoryRead to examine full entries
82
+ 3. Use MemoryEdit to update the found content
83
+
84
+ **Tips:**
85
+ - Start with simple literal patterns before using complex regex
86
+ - Use case_insensitive=true for broader matches
87
+ - Use output_mode="content" to see context around matches
88
+ - Escape special regex characters with backslash: \\. \\* \\? \\[ \\]
89
+ - Test patterns on a small set before broad searches
90
+ - Use word boundaries (\\b) for exact word matching
91
+ DESC
92
+
93
+ param :pattern,
94
+ desc: "Regular expression pattern to search for",
95
+ required: true
96
+
97
+ param :case_insensitive,
98
+ type: "boolean",
99
+ desc: "Set to true for case-insensitive search (default: false)",
100
+ required: false
101
+
102
+ param :output_mode,
103
+ desc: "Output mode: 'files_with_matches' (default), 'content', or 'count'",
104
+ required: false
105
+
106
+ # Initialize with storage instance
107
+ #
108
+ # @param storage [Core::Storage] Storage instance
109
+ def initialize(storage:)
110
+ super()
111
+ @storage = storage
112
+ end
113
+
114
+ # Override name to return simple "MemoryGrep"
115
+ def name
116
+ "MemoryGrep"
117
+ end
118
+
119
+ # Execute the tool
120
+ #
121
+ # @param pattern [String] Regex pattern to search for
122
+ # @param case_insensitive [Boolean] Whether to perform case-insensitive search
123
+ # @param output_mode [String] Output mode
124
+ # @return [String] Formatted search results
125
+ def execute(pattern:, case_insensitive: false, output_mode: "files_with_matches")
126
+ results = @storage.grep(
127
+ pattern: pattern,
128
+ case_insensitive: case_insensitive,
129
+ output_mode: output_mode,
130
+ )
131
+
132
+ format_results(results, pattern, output_mode)
133
+ rescue ArgumentError => e
134
+ validation_error(e.message)
135
+ rescue RegexpError => e
136
+ validation_error("Invalid regex pattern: #{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
+ def format_results(results, pattern, output_mode)
146
+ case output_mode
147
+ when "files_with_matches"
148
+ format_files_with_matches(results, pattern)
149
+ when "content"
150
+ format_content(results, pattern)
151
+ when "count"
152
+ format_count(results, pattern)
153
+ else
154
+ validation_error("Invalid output_mode: #{output_mode}")
155
+ end
156
+ end
157
+
158
+ def format_files_with_matches(paths, pattern)
159
+ if paths.empty?
160
+ return "No matches found for pattern '#{pattern}'"
161
+ end
162
+
163
+ result = []
164
+ result << "Memory entries matching '#{pattern}' (#{paths.size} #{paths.size == 1 ? "entry" : "entries"}):"
165
+ paths.each do |path|
166
+ result << " memory://#{path}"
167
+ end
168
+ result.join("\n")
169
+ end
170
+
171
+ def format_content(results, pattern)
172
+ if results.empty?
173
+ return "No matches found for pattern '#{pattern}'"
174
+ end
175
+
176
+ total_matches = results.sum { |r| r[:matches].size }
177
+ output = []
178
+ output << "Memory entries matching '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} #{total_matches == 1 ? "match" : "matches"}):"
179
+ output << ""
180
+
181
+ results.each do |result|
182
+ output << "memory://#{result[:path]}:"
183
+ result[:matches].each do |match|
184
+ output << " #{match[:line_number]}: #{match[:content]}"
185
+ end
186
+ output << ""
187
+ end
188
+
189
+ output.join("\n").rstrip
190
+ end
191
+
192
+ def format_count(results, pattern)
193
+ if results.empty?
194
+ return "No matches found for pattern '#{pattern}'"
195
+ end
196
+
197
+ total_matches = results.sum { |r| r[:count] }
198
+ output = []
199
+ output << "Memory entries matching '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} total #{total_matches == 1 ? "match" : "matches"}):"
200
+
201
+ results.each do |result|
202
+ output << " memory://#{result[:path]}: #{result[:count]} #{result[:count] == 1 ? "match" : "matches"}"
203
+ end
204
+
205
+ output.join("\n")
206
+ end
207
+ end
208
+ end
209
+ 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