swarm_memory 2.0.0 → 2.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9cc16f06662bd282f8c296aeb6600f53dd80d14f27afad26b9da6f724916d20a
4
- data.tar.gz: 6acf6425bdc544f125826ac5ba1f6b127f9c3d96e7ba067712cd15f165ac5af5
3
+ metadata.gz: 6bef387c2612e9bb80f24694abae15aee7b71edc30477ffd5449c83eb9d7f772
4
+ data.tar.gz: c1343d0a08ae0f7ae0514c8836fb4e7e753457b78295e3cd0c6596c1bf327d42
5
5
  SHA512:
6
- metadata.gz: 91b645763ed45723d82080fb2321ca5e9c0d61310c3f3907843c9086980f5b27e0176315b2ac399f837191c5559b6e36563e2f71bb18adaf2a93a957f186a2e9
7
- data.tar.gz: 9b019e85c71c3f7c9f1c1ac5358c53566b5c5934fb1e983087e9c6ebdd81c79b1e32c100d762161bda4aff2227c2be24bfa3d9f0784edc7cbd227a9a21d724af
6
+ metadata.gz: a60b1273dc38f2f5109754fd73b9368172dc06e844daa24a269651ff5887e10a0b066cb9f280a19b100251ce577916aa8c0fcfd26c3db1613fa4e18196dda7ce
7
+ data.tar.gz: 4543c657130eb2eaa131dba008a557ca4816b87aa5e24a54653f1fd342f298957c33dcbf8276146315b20f373a70503f12be6fb65386e49198eb4722e12908d1
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmCLI
4
- VERSION = "2.0.2"
4
+ VERSION = "2.0.3"
5
5
  end
@@ -74,8 +74,9 @@ module SwarmMemory
74
74
  # @param pattern [String] Regular expression pattern to search for
75
75
  # @param case_insensitive [Boolean] Whether to perform case-insensitive search
76
76
  # @param output_mode [String] Output mode: "files_with_matches" (default), "content", or "count"
77
+ # @param path [String, nil] Optional path prefix filter (e.g., "concept/", "fact/api-design")
77
78
  # @return [Array<Hash>, String] Results based on output_mode
78
- def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
79
+ def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches", path: nil)
79
80
  raise NotImplementedError, "Subclass must implement #grep"
80
81
  end
81
82
 
@@ -276,9 +276,9 @@ module SwarmMemory
276
276
  .reject { |f| stub_file?(f) }
277
277
 
278
278
  entries = md_files.map do |md_file|
279
- disk_path = File.basename(md_file, ".md")
280
- base_logical_path = unflatten_path(disk_path)
281
- logical_path = "#{base_logical_path}.md" # Add .md extension
279
+ # Calculate logical path relative to @directory
280
+ logical_path = md_file.sub("#{@directory}/", "")
281
+ base_logical_path = logical_path.sub(/\.md\z/, "")
282
282
 
283
283
  # Filter by prefix if provided (strip .md for comparison)
284
284
  next if prefix && !base_logical_path.start_with?(prefix.sub(/\.md\z/, ""))
@@ -287,7 +287,7 @@ module SwarmMemory
287
287
  yaml_data = File.exist?(yaml_file) ? YAML.load_file(yaml_file, permitted_classes: [Time, Date, Symbol]) : {}
288
288
 
289
289
  {
290
- path: logical_path, # With .md extension
290
+ path: logical_path,
291
291
  title: yaml_data["title"] || "Untitled",
292
292
  size: yaml_data["size"] || File.size(md_file),
293
293
  updated_at: parse_time(yaml_data["updated_at"]) || File.mtime(md_file),
@@ -304,24 +304,35 @@ module SwarmMemory
304
304
  def glob(pattern:)
305
305
  raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
306
306
 
307
- # Strip .md from pattern and flatten for disk matching
308
- base_pattern = pattern.sub(/\.md\z/, "")
309
- disk_pattern = flatten_path(base_pattern)
307
+ # Normalize pattern to ensure we only match .md files
308
+ # Standard glob behavior - just add .md extension intelligently
309
+ normalized_pattern = if pattern.end_with?("**")
310
+ # fact/** → fact/**/*.md (recursive match of all .md files)
311
+ "#{pattern}/*.md"
312
+ elsif pattern.end_with?("*")
313
+ # fact/* → fact/*.md (direct children .md files only)
314
+ "#{pattern}.md"
315
+ elsif pattern.end_with?(".md")
316
+ # Already has .md, use as-is
317
+ pattern
318
+ else
319
+ # No wildcard or extension, add .md
320
+ "#{pattern}.md"
321
+ end
310
322
 
311
- # Glob for .md files
312
- md_files = Dir.glob(File.join(@directory, "#{disk_pattern}.md"))
313
- .reject { |f| stub_file?(f) }
323
+ # Use native Dir.glob with hierarchical paths - efficient!
324
+ glob_pattern = File.join(@directory, normalized_pattern)
325
+ md_files = Dir.glob(glob_pattern).reject { |f| stub_file?(f) }
314
326
 
315
327
  results = md_files.map do |md_file|
316
- disk_path = File.basename(md_file, ".md")
317
- base_logical_path = unflatten_path(disk_path)
318
- logical_path = "#{base_logical_path}.md" # Add .md extension
328
+ # Calculate logical path relative to @directory
329
+ relative_path = md_file.sub("#{@directory}/", "")
319
330
 
320
331
  yaml_file = md_file.sub(".md", ".yml")
321
332
  yaml_data = File.exist?(yaml_file) ? YAML.load_file(yaml_file, permitted_classes: [Time, Date, Symbol]) : {}
322
333
 
323
334
  {
324
- path: logical_path, # With .md extension
335
+ path: relative_path,
325
336
  title: yaml_data["title"] || "Untitled",
326
337
  size: File.size(md_file),
327
338
  updated_at: parse_time(yaml_data["updated_at"]) || File.mtime(md_file),
@@ -340,7 +351,7 @@ module SwarmMemory
340
351
  # @param case_insensitive [Boolean] Case-insensitive search
341
352
  # @param output_mode [String] Output mode
342
353
  # @return [Array<Hash>] Results
343
- def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
354
+ def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches", path: nil)
344
355
  raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
345
356
 
346
357
  flags = case_insensitive ? Regexp::IGNORECASE : 0
@@ -348,11 +359,11 @@ module SwarmMemory
348
359
 
349
360
  case output_mode
350
361
  when "files_with_matches"
351
- grep_files_with_matches(regex)
362
+ grep_files_with_matches(regex, path)
352
363
  when "content"
353
- grep_with_content(regex)
364
+ grep_with_content(regex, path)
354
365
  when "count"
355
- grep_with_count(regex)
366
+ grep_with_count(regex, path)
356
367
  else
357
368
  raise ArgumentError, "Invalid output_mode: #{output_mode}"
358
369
  end
@@ -506,17 +517,22 @@ module SwarmMemory
506
517
  #
507
518
  # @param logical_path [String] Logical path with slashes
508
519
  # @return [String] Flattened path with --
520
+ # Identity function - paths are now stored hierarchically
521
+ # Kept for backward compatibility during transition
522
+ #
523
+ # @param logical_path [String] Logical path
524
+ # @return [String] Same path (no flattening)
509
525
  def flatten_path(logical_path)
510
- logical_path.gsub("/", "--")
526
+ logical_path
511
527
  end
512
528
 
513
- # Unflatten path from disk storage
514
- # "concepts--ruby--classes" "concepts/ruby/classes"
529
+ # Identity function - paths are now stored hierarchically
530
+ # Kept for backward compatibility during transition
515
531
  #
516
- # @param disk_path [String] Flattened path
517
- # @return [String] Logical path with slashes
532
+ # @param disk_path [String] Disk path
533
+ # @return [String] Same path (no unflattening)
518
534
  def unflatten_path(disk_path)
519
- disk_path.gsub("--", "/")
535
+ disk_path
520
536
  end
521
537
 
522
538
  # Check if content is a stub (redirect)
@@ -622,9 +638,12 @@ module SwarmMemory
622
638
  Dir.glob(File.join(@directory, "**/*.md")).each do |md_file|
623
639
  next if stub_file?(md_file)
624
640
 
625
- disk_path = File.basename(md_file, ".md")
626
- base_logical_path = unflatten_path(disk_path)
627
- logical_path = "#{base_logical_path}.md" # Add .md extension
641
+ # Calculate logical path relative to @directory
642
+ logical_path = md_file.sub("#{@directory}/", "")
643
+ base_logical_path = logical_path.sub(/\.md\z/, "")
644
+
645
+ # disk_path is now the same as base_logical_path (no flattening)
646
+ disk_path = base_logical_path
628
647
 
629
648
  yaml_file = md_file.sub(".md", ".yml")
630
649
  yaml_data = File.exist?(yaml_file) ? YAML.load_file(yaml_file, permitted_classes: [Time, Date, Symbol]) : {}
@@ -648,19 +667,21 @@ module SwarmMemory
648
667
  #
649
668
  # @param regex [Regexp] Pattern to match
650
669
  # @return [Array<String>] Matching logical paths with .md extension
651
- def grep_files_with_matches(regex)
670
+ def grep_files_with_matches(regex, path_filter = nil)
652
671
  results = []
653
672
 
654
673
  # Fast path: Search .yml files (metadata)
655
674
  Dir.glob(File.join(@directory, "**/*.yml")).each do |yaml_file|
656
675
  next if yaml_file.include?("_stubs/")
657
676
 
677
+ # Calculate logical path relative to @directory
678
+ logical_path = yaml_file.sub("#{@directory}/", "").sub(".yml", ".md")
679
+ next unless matches_path_filter?(logical_path, path_filter)
680
+
658
681
  content = File.read(yaml_file)
659
682
  next unless regex.match?(content)
660
683
 
661
- disk_path = File.basename(yaml_file, ".yml")
662
- base_path = unflatten_path(disk_path)
663
- results << "#{base_path}.md" # Add .md extension
684
+ results << logical_path
664
685
  end
665
686
 
666
687
  # If found in metadata, return quickly
@@ -670,12 +691,14 @@ module SwarmMemory
670
691
  Dir.glob(File.join(@directory, "**/*.md")).each do |md_file|
671
692
  next if stub_file?(md_file)
672
693
 
694
+ # Calculate logical path relative to @directory
695
+ logical_path = md_file.sub("#{@directory}/", "")
696
+ next unless matches_path_filter?(logical_path, path_filter)
697
+
673
698
  content = File.read(md_file)
674
699
  next unless regex.match?(content)
675
700
 
676
- disk_path = File.basename(md_file, ".md")
677
- base_path = unflatten_path(disk_path)
678
- results << "#{base_path}.md" # Add .md extension
701
+ results << logical_path
679
702
  end
680
703
 
681
704
  results.uniq.sort
@@ -684,13 +707,18 @@ module SwarmMemory
684
707
  # Grep with content and line numbers
685
708
  #
686
709
  # @param regex [Regexp] Pattern to match
710
+ # @param path_filter [String, nil] Optional path prefix filter
687
711
  # @return [Array<Hash>] Results with matches
688
- def grep_with_content(regex)
712
+ def grep_with_content(regex, path_filter = nil)
689
713
  results = []
690
714
 
691
715
  Dir.glob(File.join(@directory, "**/*.md")).each do |md_file|
692
716
  next if stub_file?(md_file)
693
717
 
718
+ # Calculate logical path relative to @directory
719
+ logical_path = md_file.sub("#{@directory}/", "")
720
+ next unless matches_path_filter?(logical_path, path_filter)
721
+
694
722
  content = File.read(md_file)
695
723
  matching_lines = []
696
724
 
@@ -700,12 +728,8 @@ module SwarmMemory
700
728
 
701
729
  next if matching_lines.empty?
702
730
 
703
- disk_path = File.basename(md_file, ".md")
704
- base_path = unflatten_path(disk_path)
705
- logical_path = "#{base_path}.md" # Add .md extension
706
-
707
731
  results << {
708
- path: logical_path, # With .md extension
732
+ path: logical_path,
709
733
  matches: matching_lines,
710
734
  }
711
735
  end
@@ -716,24 +740,25 @@ module SwarmMemory
716
740
  # Grep with match counts
717
741
  #
718
742
  # @param regex [Regexp] Pattern to match
743
+ # @param path_filter [String, nil] Optional path prefix filter
719
744
  # @return [Array<Hash>] Results with counts
720
- def grep_with_count(regex)
745
+ def grep_with_count(regex, path_filter = nil)
721
746
  results = []
722
747
 
723
748
  Dir.glob(File.join(@directory, "**/*.md")).each do |md_file|
724
749
  next if stub_file?(md_file)
725
750
 
751
+ # Calculate logical path relative to @directory
752
+ logical_path = md_file.sub("#{@directory}/", "")
753
+ next unless matches_path_filter?(logical_path, path_filter)
754
+
726
755
  content = File.read(md_file)
727
756
  count = content.scan(regex).size
728
757
 
729
758
  next if count <= 0
730
759
 
731
- disk_path = File.basename(md_file, ".md")
732
- base_path = unflatten_path(disk_path)
733
- logical_path = "#{base_path}.md" # Add .md extension
734
-
735
760
  results << {
736
- path: logical_path, # With .md extension
761
+ path: logical_path,
737
762
  count: count,
738
763
  }
739
764
  end
@@ -741,6 +766,37 @@ module SwarmMemory
741
766
  results
742
767
  end
743
768
 
769
+ # Check if a logical path matches the filter
770
+ #
771
+ # Behaves like directory/file filtering even though paths are logical.
772
+ #
773
+ # @param logical_path [String] The logical path to check (e.g., "concept/ruby/blocks.md")
774
+ # @param path_filter [String, nil] Optional path prefix filter (e.g., "concept/", "fact/api-design", "skill/ruby/lambdas.md")
775
+ # @return [Boolean] True if path matches or no filter specified
776
+ #
777
+ # @example Directory-style filtering
778
+ # matches_path_filter?("concept/ruby/blocks.md", "concept/") #=> true
779
+ # matches_path_filter?("concept/ruby/blocks.md", "concept") #=> true
780
+ # matches_path_filter?("fact/api-design/rest.md", "fact/api") #=> false (requires "fact/api/")
781
+ # matches_path_filter?("fact/api/rest-basics.md", "fact/api") #=> true
782
+ #
783
+ # @example File-specific filtering
784
+ # matches_path_filter?("concept/ruby/blocks.md", "concept/ruby/blocks.md") #=> true (exact match)
785
+ # matches_path_filter?("concept/ruby/lambdas.md", "concept/ruby/blocks.md") #=> false
786
+ def matches_path_filter?(logical_path, path_filter)
787
+ return true if path_filter.nil? || path_filter.empty?
788
+
789
+ # If filter specifies a file (ends with .md), do exact match
790
+ return logical_path == path_filter if path_filter.end_with?(".md")
791
+
792
+ # Otherwise, treat as directory path
793
+ # Normalize: ensure filter ends with "/" for proper directory matching
794
+ # This prevents "fact/api" from matching "fact/api-design/"
795
+ dir_filter = path_filter.end_with?("/") ? path_filter : "#{path_filter}/"
796
+
797
+ logical_path.start_with?(dir_filter)
798
+ end
799
+
744
800
  # Calculate checksum for embedding
745
801
  #
746
802
  # @param embedding [Array<Float>] Embedding vector
@@ -143,12 +143,14 @@ module SwarmMemory
143
143
  # @param pattern [String] Regex pattern
144
144
  # @param case_insensitive [Boolean] Case-insensitive search
145
145
  # @param output_mode [String] Output mode
146
+ # @param path [String, nil] Optional path prefix filter
146
147
  # @return [Array<Hash>] Search results
147
- def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
148
+ def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches", path: nil)
148
149
  @adapter.grep(
149
150
  pattern: pattern,
150
151
  case_insensitive: case_insensitive,
151
152
  output_mode: output_mode,
153
+ path: path,
152
154
  )
153
155
  end
154
156
 
@@ -7,7 +7,9 @@ You have persistent memory that learns from conversations and helps you answer q
7
7
  **When user says "learn about X" or "research X":**
8
8
  1. Gather information (read docs, ask questions, etc.)
9
9
  2. **STORE your findings in memory** using MemoryWrite
10
- 3. Categorize as fact/concept/skill/experience
10
+ 3. **Be THOROUGH** - Capture all important details, don't summarize away key information
11
+ 4. **Split if needed** - If content is large, create multiple focused, linked memories
12
+ 5. Categorize as fact/concept/skill/experience
11
13
 
12
14
  **"Learning" is NOT complete until you've stored it in memory.**
13
15
 
@@ -16,7 +18,7 @@ You have persistent memory that learns from conversations and helps you answer q
16
18
  - "Find out who's the commander" → Discover it → MemoryWrite(type: "fact", ...)
17
19
  - "Learn this procedure" → Understand it → MemoryWrite(type: "skill", ...)
18
20
 
19
- **Learning = Understanding + Storing. Always do both.**
21
+ **Learning = Understanding + Thorough Storage. Always do both.**
20
22
 
21
23
  ## Your Memory Tools (Use ONLY These)
22
24
 
@@ -79,6 +81,42 @@ User says: "Save a skill called 'Eclipse power prep' with these steps..."
79
81
 
80
82
  **Don't consolidate.** Separate memories are more searchable.
81
83
 
84
+ ## CRITICAL: Be Thorough But Split Large Content
85
+
86
+ **IMPORTANT: Memories are NOT summaries - they are FULL, DETAILED records.**
87
+
88
+ **When storing information, you MUST:**
89
+
90
+ 1. **Be THOROUGH** - Don't miss any details, facts, or nuances
91
+ 2. **Store COMPLETE information** - Not just bullet points or summaries
92
+ 3. **Include ALL relevant details** - Code examples, specific values, exact procedures
93
+ 4. **Keep each memory FOCUSED** - If content is getting long, split it
94
+ 5. **Link related memories** - Use the `related` metadata field
95
+
96
+ **What this means:**
97
+ - ❌ "The payment system has several validation steps" (too vague)
98
+ - ✅ "The payment system validates: 1) Card number format (Luhn algorithm), 2) CVV length (3-4 digits depending on card type), 3) Expiration date (must be future date), 4) Billing address match via AVS..." (complete details)
99
+
100
+ **If content is too large:**
101
+ - ✅ Split into multiple focused memories
102
+ - ✅ Each memory covers one specific aspect IN DETAIL
103
+ - ✅ Link them together using `related` field
104
+ - ❌ Don't create one huge memory that's hard to search
105
+ - ❌ Don't summarize to make it fit - split instead
106
+
107
+ **Example - Learning about a complex system:**
108
+
109
+ Instead of one giant memory:
110
+ ❌ `concept/payment-system.md` (5000 words covering everything)
111
+
112
+ Create multiple linked memories with FULL details in each:
113
+ ✅ `concept/payment/processing-flow.md` (complete flow with all steps) → related: ["concept/payment/validation.md"]
114
+ ✅ `concept/payment/validation.md` (all validation rules with specifics) → related: ["concept/payment/processing-flow.md", "concept/payment/error-handling.md"]
115
+ ✅ `concept/payment/error-handling.md` (all error codes and responses) → related: ["concept/payment/validation.md"]
116
+ ✅ `concept/payment/security.md` (all security measures and protocols) → related: ["concept/payment/validation.md"]
117
+
118
+ **The goal: Capture EVERYTHING with full details, but keep each memory focused and searchable.**
119
+
82
120
  ## When to Use LoadSkill vs MemoryRead
83
121
 
84
122
  **CRITICAL - LoadSkill is for DOING, not for explaining:**
@@ -110,8 +148,10 @@ User says: "Save a skill called 'Eclipse power prep' with these steps..."
110
148
  **When user teaches you:**
111
149
  1. Listen to what they're saying
112
150
  2. Identify the type (fact/concept/skill/experience)
113
- 3. MemoryWrite with proper type and metadata
114
- 4. Continue conversation naturally
151
+ 3. **Capture ALL details** - Don't skip anything important
152
+ 4. If content is large, split into multiple related memories
153
+ 5. MemoryWrite with proper type, metadata, and `related` links
154
+ 6. Continue conversation naturally
115
155
 
116
156
  **When user asks a question:**
117
157
  1. Check auto-surfaced memories (including skills)
@@ -132,8 +172,10 @@ User says: "Save a skill called 'Eclipse power prep' with these steps..."
132
172
  - `title` - Brief description
133
173
  - `tags` - Searchable keywords
134
174
  - `domain` - Category (e.g., "people", "thermal/systems")
135
- - `related` - Empty array `[]` if none
175
+ - `related` - **IMPORTANT**: Link related memories (e.g., ["concept/payment/validation.md"]). Use this to connect split memories and related topics. Empty array `[]` only if truly isolated.
136
176
  - `confidence` - Defaults to "medium" if omitted
137
177
  - `source` - Defaults to "user" if omitted
138
178
 
139
179
  **Be natural in conversation. Store knowledge efficiently. Create skills when user describes procedures.**
180
+
181
+ IMPORTANT: For optimal performance, make all tool calls in parallel when you can.
@@ -74,3 +74,5 @@ If memories are about Project X, assume questions are about Project X.
74
74
  If memories are about Ruby code, assume code questions are about Ruby.
75
75
 
76
76
  **Every question requires memory access. Be efficient and accurate.**
77
+
78
+ IMPORTANT: For optimal performance, make all tool calls in parallel when you can.
@@ -27,12 +27,14 @@ module SwarmMemory
27
27
  # @param pattern [String] Regex pattern
28
28
  # @param case_insensitive [Boolean] Case-insensitive search
29
29
  # @param output_mode [String] Output mode
30
+ # @param path [String, nil] Optional path prefix filter
30
31
  # @return [Array<Hash>] Search results
31
- def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
32
+ def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches", path: nil)
32
33
  @adapter.grep(
33
34
  pattern: pattern,
34
35
  case_insensitive: case_insensitive,
35
36
  output_mode: output_mode,
37
+ path: path,
36
38
  )
37
39
  end
38
40
  end
@@ -15,18 +15,20 @@ module SwarmMemory
15
15
  **Parameters:**
16
16
  - pattern (REQUIRED): Glob pattern with wildcards (e.g., '**/*.txt', 'parallel/*/task_*', 'skill/**')
17
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/**')
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
21
  - `?` - matches any single character (e.g., 'task_?')
22
22
  - `[abc]` - matches any character in the set (e.g., 'task_[0-9]')
23
23
 
24
24
  **Returns:**
25
- List of matching entries with:
25
+ List of matching .md memory entries with:
26
26
  - Full memory:// path
27
27
  - Entry title
28
28
  - Size in bytes/KB/MB
29
29
 
30
+ **Note**: Only returns .md files (actual memory entries), not directory entries.
31
+
30
32
  **MEMORY STRUCTURE (4 Fixed Categories Only):**
31
33
  ALL patterns MUST target one of these 4 categories:
32
34
  - concept/{domain}/** - Abstract ideas
@@ -37,7 +39,15 @@ module SwarmMemory
37
39
 
38
40
  **Common Use Cases:**
39
41
  ```
40
- # Find all skills
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
41
51
  MemoryGlob(pattern: "skill/**")
42
52
  Result: skill/debugging/api-errors.md, skill/meta/deep-learning.md, ...
43
53
 
@@ -45,23 +55,28 @@ module SwarmMemory
45
55
  MemoryGlob(pattern: "concept/ruby/**")
46
56
  Result: concept/ruby/classes.md, concept/ruby/modules.md, ...
47
57
 
48
- # Find all facts about people
58
+ # Find direct files in fact/people/
49
59
  MemoryGlob(pattern: "fact/people/*")
50
- Result: fact/people/john.md, fact/people/jane.md, ...
60
+ Result: fact/people/john.md, fact/people/jane.md (not fact/people/teams/x.md)
51
61
 
52
62
  # Find all experiences
53
63
  MemoryGlob(pattern: "experience/**")
54
64
  Result: experience/fixed-cors-bug.md, experience/optimization.md, ...
55
65
 
56
- # Find debugging skills
57
- MemoryGlob(pattern: "skill/debugging/*")
66
+ # Find debugging skills recursively
67
+ MemoryGlob(pattern: "skill/debugging/**")
58
68
  Result: skill/debugging/api-errors.md, skill/debugging/performance.md, ...
59
69
 
60
70
  # Find all entries (all categories)
61
71
  MemoryGlob(pattern: "**/*")
62
- Result: All entries across all 4 categories
72
+ Result: All .md entries across all 4 categories
63
73
  ```
64
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
+
65
80
  **When to Use MemoryGlob:**
66
81
  - Discovering what's in a memory hierarchy
67
82
  - Finding all entries matching a naming convention
@@ -19,6 +19,7 @@ module SwarmMemory
19
19
  - pattern (REQUIRED): Regular expression pattern to search for (e.g., 'status: pending', 'TODO.*urgent', '\\btask_\\d+\\b')
20
20
 
21
21
  **Optional Parameters:**
22
+ - path: Limit search to specific path (e.g., 'concept/', 'fact/api-design/', 'skill/ruby')
22
23
  - case_insensitive: Set to true for case-insensitive search (default: false)
23
24
  - output_mode: Choose output format - 'files_with_matches' (default), 'content', or 'count'
24
25
 
@@ -44,22 +45,38 @@ module SwarmMemory
44
45
  - Quantifiers: '*' (0+), '+' (1+), '?' (0 or 1), '{3}' (exactly 3)
45
46
  - Alternation: 'pending|in-progress|blocked'
46
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
+
47
55
  **Examples:**
48
56
  ```
49
57
  # Find entries containing "TODO" (case-sensitive)
50
58
  MemoryGrep(pattern: "TODO")
51
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
+
52
69
  # Find entries with any status (case-insensitive)
53
70
  MemoryGrep(pattern: "status:", case_insensitive: true)
54
71
 
55
- # Show actual content of matches
56
- MemoryGrep(pattern: "error|warning|failed", output_mode: "content")
72
+ # Show actual content of matches in skills only
73
+ MemoryGrep(pattern: "error|warning|failed", path: "skill/", output_mode: "content")
57
74
 
58
- # Count how many times "completed" appears in each entry
59
- MemoryGrep(pattern: "completed", output_mode: "count")
75
+ # Count how many times "completed" appears in experiences
76
+ MemoryGrep(pattern: "completed", path: "experience/", output_mode: "count")
60
77
 
61
- # Find task numbers
62
- MemoryGrep(pattern: "task_\\d+")
78
+ # Find task numbers in facts
79
+ MemoryGrep(pattern: "task_\\d+", path: "fact/")
63
80
 
64
81
  # Find incomplete tasks
65
82
  MemoryGrep(pattern: "^- \\[ \\]", output_mode: "content")
@@ -84,6 +101,7 @@ module SwarmMemory
84
101
  **Tips:**
85
102
  - Start with simple literal patterns before using complex regex
86
103
  - Use case_insensitive=true for broader matches
104
+ - Use path parameter to limit search scope (faster and more precise)
87
105
  - Use output_mode="content" to see context around matches
88
106
  - Escape special regex characters with backslash: \\. \\* \\? \\[ \\]
89
107
  - Test patterns on a small set before broad searches
@@ -94,6 +112,10 @@ module SwarmMemory
94
112
  desc: "Regular expression pattern to search for",
95
113
  required: true
96
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
+
97
119
  param :case_insensitive,
98
120
  type: "boolean",
99
121
  desc: "Set to true for case-insensitive search (default: false)",
@@ -119,17 +141,19 @@ module SwarmMemory
119
141
  # Execute the tool
120
142
  #
121
143
  # @param pattern [String] Regex pattern to search for
144
+ # @param path [String, nil] Optional path filter
122
145
  # @param case_insensitive [Boolean] Whether to perform case-insensitive search
123
146
  # @param output_mode [String] Output mode
124
147
  # @return [String] Formatted search results
125
- def execute(pattern:, case_insensitive: false, output_mode: "files_with_matches")
148
+ def execute(pattern:, path: nil, case_insensitive: false, output_mode: "files_with_matches")
126
149
  results = @storage.grep(
127
150
  pattern: pattern,
151
+ path: path,
128
152
  case_insensitive: case_insensitive,
129
153
  output_mode: output_mode,
130
154
  )
131
155
 
132
- format_results(results, pattern, output_mode)
156
+ format_results(results, pattern, output_mode, path)
133
157
  rescue ArgumentError => e
134
158
  validation_error(e.message)
135
159
  rescue RegexpError => e
@@ -142,40 +166,52 @@ module SwarmMemory
142
166
  "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
143
167
  end
144
168
 
145
- def format_results(results, pattern, output_mode)
169
+ def format_results(results, pattern, output_mode, path_filter)
146
170
  case output_mode
147
171
  when "files_with_matches"
148
- format_files_with_matches(results, pattern)
172
+ format_files_with_matches(results, pattern, path_filter)
149
173
  when "content"
150
- format_content(results, pattern)
174
+ format_content(results, pattern, path_filter)
151
175
  when "count"
152
- format_count(results, pattern)
176
+ format_count(results, pattern, path_filter)
153
177
  else
154
178
  validation_error("Invalid output_mode: #{output_mode}")
155
179
  end
156
180
  end
157
181
 
158
- def format_files_with_matches(paths, pattern)
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
+
159
193
  if paths.empty?
160
- return "No matches found for pattern '#{pattern}'"
194
+ return "No matches found for pattern #{search_desc}"
161
195
  end
162
196
 
163
197
  result = []
164
- result << "Memory entries matching '#{pattern}' (#{paths.size} #{paths.size == 1 ? "entry" : "entries"}):"
198
+ result << "Memory entries matching #{search_desc} (#{paths.size} #{paths.size == 1 ? "entry" : "entries"}):"
165
199
  paths.each do |path|
166
200
  result << " memory://#{path}"
167
201
  end
168
202
  result.join("\n")
169
203
  end
170
204
 
171
- def format_content(results, pattern)
205
+ def format_content(results, pattern, path_filter)
206
+ search_desc = format_search_header(pattern, path_filter)
207
+
172
208
  if results.empty?
173
- return "No matches found for pattern '#{pattern}'"
209
+ return "No matches found for pattern #{search_desc}"
174
210
  end
175
211
 
176
212
  total_matches = results.sum { |r| r[:matches].size }
177
213
  output = []
178
- output << "Memory entries matching '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} #{total_matches == 1 ? "match" : "matches"}):"
214
+ output << "Memory entries matching #{search_desc} (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} #{total_matches == 1 ? "match" : "matches"}):"
179
215
  output << ""
180
216
 
181
217
  results.each do |result|
@@ -189,14 +225,16 @@ module SwarmMemory
189
225
  output.join("\n").rstrip
190
226
  end
191
227
 
192
- def format_count(results, pattern)
228
+ def format_count(results, pattern, path_filter)
229
+ search_desc = format_search_header(pattern, path_filter)
230
+
193
231
  if results.empty?
194
- return "No matches found for pattern '#{pattern}'"
232
+ return "No matches found for pattern #{search_desc}"
195
233
  end
196
234
 
197
235
  total_matches = results.sum { |r| r[:count] }
198
236
  output = []
199
- output << "Memory entries matching '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} total #{total_matches == 1 ? "match" : "matches"}):"
237
+ output << "Memory entries matching #{search_desc} (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} total #{total_matches == 1 ? "match" : "matches"}):"
200
238
 
201
239
  results.each do |result|
202
240
  output << " memory://#{result[:path]}: #{result[:count]} #{result[:count] == 1 ? "match" : "matches"}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmMemory
4
- VERSION = "2.0.0"
4
+ VERSION = "2.1.0"
5
5
  end
@@ -298,7 +298,7 @@ module SwarmSDK
298
298
  # Check if a tool should be disabled based on disable_default_tools config
299
299
  #
300
300
  # @param tool_name [Symbol] Tool name to check
301
- # @param disable_config [nil, Boolean, Array<Symbol>] Disable configuration
301
+ # @param disable_config [nil, Boolean, Symbol, Array<Symbol>] Disable configuration
302
302
  # @return [Boolean] True if tool should be disabled
303
303
  def tool_disabled?(tool_name, disable_config)
304
304
  return false if disable_config.nil?
@@ -306,6 +306,9 @@ module SwarmSDK
306
306
  if disable_config == true
307
307
  # Disable all default tools
308
308
  true
309
+ elsif disable_config.is_a?(Symbol)
310
+ # Single tool name
311
+ disable_config == tool_name
309
312
  elsif disable_config.is_a?(Array)
310
313
  # Disable only tools in the array
311
314
  disable_config.include?(tool_name)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.0.6"
4
+ VERSION = "2.1.0"
5
5
  end
data/lib/swarm_sdk.rb CHANGED
@@ -147,15 +147,6 @@ require "ruby_llm/mcp/parameter"
147
147
 
148
148
  module RubyLLM
149
149
  module MCP
150
- class Parameter < RubyLLM::Parameter
151
- def initialize(name, type: "string", desc: nil, required: true, default: nil, union_type: nil)
152
- super(name, type: type, desc: desc, required: required)
153
- @properties = {}
154
- @union_type = union_type
155
- @default = default
156
- end
157
- end
158
-
159
150
  module Notifications
160
151
  class Initialize
161
152
  def call
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swarm_memory
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-10-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async
@@ -307,7 +307,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
307
307
  - !ruby/object:Gem::Version
308
308
  version: '0'
309
309
  requirements: []
310
- rubygems_version: 3.6.2
310
+ rubygems_version: 3.6.9
311
311
  specification_version: 4
312
312
  summary: Persistent memory system for SwarmSDK agents
313
313
  test_files: []