claude_swarm 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/release.md +1 -1
  3. data/.claude/hooks/lint-code-files.rb +65 -0
  4. data/.rubocop.yml +22 -2
  5. data/CHANGELOG.md +14 -1
  6. data/CLAUDE.md +1 -1
  7. data/CONTRIBUTING.md +69 -0
  8. data/README.md +27 -2
  9. data/Rakefile +71 -3
  10. data/analyze_coverage.rb +94 -0
  11. data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
  12. data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
  13. data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
  14. data/docs/v2/README.md +308 -0
  15. data/docs/v2/guides/claude-code-agents.md +262 -0
  16. data/docs/v2/guides/complete-tutorial.md +3088 -0
  17. data/docs/v2/guides/getting-started.md +1456 -0
  18. data/docs/v2/guides/memory-adapters.md +998 -0
  19. data/docs/v2/guides/plugins.md +816 -0
  20. data/docs/v2/guides/quick-start-cli.md +1745 -0
  21. data/docs/v2/guides/rails-integration.md +1902 -0
  22. data/docs/v2/guides/swarm-memory.md +599 -0
  23. data/docs/v2/reference/cli.md +729 -0
  24. data/docs/v2/reference/ruby-dsl.md +2154 -0
  25. data/docs/v2/reference/yaml.md +1835 -0
  26. data/docs-team-swarm.yml +2222 -0
  27. data/examples/learning-assistant/assistant.md +7 -0
  28. data/examples/learning-assistant/example-memories/concept-example.md +90 -0
  29. data/examples/learning-assistant/example-memories/experience-example.md +66 -0
  30. data/examples/learning-assistant/example-memories/fact-example.md +76 -0
  31. data/examples/learning-assistant/example-memories/memory-index.md +78 -0
  32. data/examples/learning-assistant/example-memories/skill-example.md +168 -0
  33. data/examples/learning-assistant/learning_assistant.rb +34 -0
  34. data/examples/learning-assistant/learning_assistant.yml +20 -0
  35. data/examples/v2/dsl/01_basic.rb +44 -0
  36. data/examples/v2/dsl/02_core_parameters.rb +59 -0
  37. data/examples/v2/dsl/03_capabilities.rb +71 -0
  38. data/examples/v2/dsl/04_llm_parameters.rb +56 -0
  39. data/examples/v2/dsl/05_advanced_flags.rb +73 -0
  40. data/examples/v2/dsl/06_permissions.rb +80 -0
  41. data/examples/v2/dsl/07_mcp_server.rb +62 -0
  42. data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
  43. data/examples/v2/dsl/09_agent_hooks.rb +67 -0
  44. data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
  45. data/examples/v2/dsl/11_delegation.rb +60 -0
  46. data/examples/v2/dsl/12_complete_integration.rb +137 -0
  47. data/examples/v2/file_tools_swarm.yml +102 -0
  48. data/examples/v2/hooks/01_basic_hooks.rb +133 -0
  49. data/examples/v2/hooks/02_usage_tracking.rb +201 -0
  50. data/examples/v2/hooks/03_production_monitoring.rb +429 -0
  51. data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
  52. data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
  53. data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
  54. data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
  55. data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
  56. data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
  57. data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
  58. data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
  59. data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
  60. data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
  61. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
  62. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
  63. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
  64. data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
  65. data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
  66. data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
  67. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
  68. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
  69. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
  70. data/examples/v2/hooks/swarm_summary.sh +44 -0
  71. data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
  72. data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
  73. data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
  74. data/examples/v2/hooks/validate_bash.rb +59 -0
  75. data/examples/v2/multi_directory_permissions.yml +221 -0
  76. data/examples/v2/node_context_demo.rb +127 -0
  77. data/examples/v2/node_workflow.rb +173 -0
  78. data/examples/v2/path_resolution_demo.rb +216 -0
  79. data/examples/v2/simple-swarm-v2.rb +90 -0
  80. data/examples/v2/simple-swarm-v2.yml +62 -0
  81. data/examples/v2/swarm.yml +71 -0
  82. data/examples/v2/swarm_with_hooks.yml +61 -0
  83. data/examples/v2/swarm_with_hooks_simple.yml +25 -0
  84. data/examples/v2/think_tool_demo.rb +62 -0
  85. data/exe/swarm +6 -0
  86. data/lib/claude_swarm/claude_mcp_server.rb +0 -6
  87. data/lib/claude_swarm/cli.rb +10 -3
  88. data/lib/claude_swarm/commands/ps.rb +19 -20
  89. data/lib/claude_swarm/commands/show.rb +1 -1
  90. data/lib/claude_swarm/configuration.rb +10 -12
  91. data/lib/claude_swarm/mcp_generator.rb +10 -1
  92. data/lib/claude_swarm/orchestrator.rb +73 -49
  93. data/lib/claude_swarm/system_utils.rb +37 -11
  94. data/lib/claude_swarm/version.rb +1 -1
  95. data/lib/claude_swarm/worktree_manager.rb +1 -0
  96. data/lib/claude_swarm/yaml_loader.rb +22 -0
  97. data/lib/claude_swarm.rb +7 -2
  98. data/lib/swarm_cli/cli.rb +201 -0
  99. data/lib/swarm_cli/command_registry.rb +61 -0
  100. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  101. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  102. data/lib/swarm_cli/commands/migrate.rb +55 -0
  103. data/lib/swarm_cli/commands/run.rb +173 -0
  104. data/lib/swarm_cli/config_loader.rb +97 -0
  105. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  106. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  107. data/lib/swarm_cli/interactive_repl.rb +918 -0
  108. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  109. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  110. data/lib/swarm_cli/migrate_options.rb +54 -0
  111. data/lib/swarm_cli/migrator.rb +132 -0
  112. data/lib/swarm_cli/options.rb +151 -0
  113. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  114. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  115. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  116. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  117. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  118. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  119. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  120. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  121. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  122. data/lib/swarm_cli/ui/icons.rb +59 -0
  123. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  124. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  125. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  126. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  127. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  128. data/lib/swarm_cli/version.rb +5 -0
  129. data/lib/swarm_cli.rb +44 -0
  130. data/lib/swarm_memory/adapters/base.rb +141 -0
  131. data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
  132. data/lib/swarm_memory/chat_extension.rb +34 -0
  133. data/lib/swarm_memory/cli/commands.rb +306 -0
  134. data/lib/swarm_memory/core/entry.rb +37 -0
  135. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  136. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  137. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  138. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  139. data/lib/swarm_memory/core/storage.rb +288 -0
  140. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  141. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  142. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  143. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  144. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  145. data/lib/swarm_memory/errors.rb +21 -0
  146. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  147. data/lib/swarm_memory/integration/configuration.rb +43 -0
  148. data/lib/swarm_memory/integration/registration.rb +31 -0
  149. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  150. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  151. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  152. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  153. data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
  154. data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
  155. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
  156. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  157. data/lib/swarm_memory/search/text_search.rb +42 -0
  158. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  159. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  160. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  161. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  162. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  163. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  164. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  165. data/lib/swarm_memory/tools/memory_glob.rb +160 -0
  166. data/lib/swarm_memory/tools/memory_grep.rb +247 -0
  167. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  168. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  169. data/lib/swarm_memory/tools/memory_write.rb +231 -0
  170. data/lib/swarm_memory/utils.rb +50 -0
  171. data/lib/swarm_memory/version.rb +5 -0
  172. data/lib/swarm_memory.rb +166 -0
  173. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  174. data/lib/swarm_sdk/agent/builder.rb +461 -0
  175. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  176. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  177. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  178. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  179. data/lib/swarm_sdk/agent/chat.rb +1159 -0
  180. data/lib/swarm_sdk/agent/context.rb +112 -0
  181. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  182. data/lib/swarm_sdk/agent/definition.rb +556 -0
  183. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  184. data/lib/swarm_sdk/configuration.rb +296 -0
  185. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  186. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  187. data/lib/swarm_sdk/context_compactor.rb +340 -0
  188. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  189. data/lib/swarm_sdk/hooks/context.rb +197 -0
  190. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  191. data/lib/swarm_sdk/hooks/error.rb +29 -0
  192. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  193. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  194. data/lib/swarm_sdk/hooks/result.rb +150 -0
  195. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  196. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  197. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  198. data/lib/swarm_sdk/log_collector.rb +51 -0
  199. data/lib/swarm_sdk/log_stream.rb +69 -0
  200. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  201. data/lib/swarm_sdk/model_aliases.json +5 -0
  202. data/lib/swarm_sdk/models.json +1 -0
  203. data/lib/swarm_sdk/models.rb +120 -0
  204. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  205. data/lib/swarm_sdk/node/builder.rb +439 -0
  206. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  207. data/lib/swarm_sdk/node_context.rb +170 -0
  208. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  209. data/lib/swarm_sdk/permissions/config.rb +239 -0
  210. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  211. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  212. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  213. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  214. data/lib/swarm_sdk/plugin.rb +147 -0
  215. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  216. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  217. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  218. data/lib/swarm_sdk/result.rb +97 -0
  219. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  220. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  221. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  222. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  223. data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
  224. data/lib/swarm_sdk/swarm.rb +982 -0
  225. data/lib/swarm_sdk/tools/bash.rb +274 -0
  226. data/lib/swarm_sdk/tools/clock.rb +44 -0
  227. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  228. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  229. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  230. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  231. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  232. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  233. data/lib/swarm_sdk/tools/edit.rb +150 -0
  234. data/lib/swarm_sdk/tools/glob.rb +158 -0
  235. data/lib/swarm_sdk/tools/grep.rb +228 -0
  236. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  237. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  238. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  239. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  240. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  241. data/lib/swarm_sdk/tools/read.rb +251 -0
  242. data/lib/swarm_sdk/tools/registry.rb +93 -0
  243. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  244. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  245. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  246. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  247. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  248. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  249. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  250. data/lib/swarm_sdk/tools/think.rb +95 -0
  251. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  252. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  253. data/lib/swarm_sdk/tools/write.rb +117 -0
  254. data/lib/swarm_sdk/utils.rb +50 -0
  255. data/lib/swarm_sdk/version.rb +5 -0
  256. data/lib/swarm_sdk.rb +157 -0
  257. data/llm.v2.txt +13407 -0
  258. data/rubocop/cop/security/no_reflection_methods.rb +47 -0
  259. data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
  260. data/swarm_cli.gemspec +57 -0
  261. data/swarm_memory.gemspec +28 -0
  262. data/swarm_sdk.gemspec +41 -0
  263. data/team.yml +1 -1
  264. data/team_full.yml +1875 -0
  265. data/{team_v2.yml → team_sdk.yml} +121 -52
  266. metadata +249 -6
  267. 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