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 +4 -4
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/base.rb +2 -1
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +102 -46
- data/lib/swarm_memory/core/storage.rb +3 -1
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +47 -5
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +2 -0
- data/lib/swarm_memory/search/text_search.rb +3 -1
- data/lib/swarm_memory/tools/memory_glob.rb +25 -10
- data/lib/swarm_memory/tools/memory_grep.rb +59 -21
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_sdk/swarm/tool_configurator.rb +4 -1
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +0 -9
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6bef387c2612e9bb80f24694abae15aee7b71edc30477ffd5449c83eb9d7f772
|
|
4
|
+
data.tar.gz: c1343d0a08ae0f7ae0514c8836fb4e7e753457b78295e3cd0c6596c1bf327d42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a60b1273dc38f2f5109754fd73b9368172dc06e844daa24a269651ff5887e10a0b066cb9f280a19b100251ce577916aa8c0fcfd26c3db1613fa4e18196dda7ce
|
|
7
|
+
data.tar.gz: 4543c657130eb2eaa131dba008a557ca4816b87aa5e24a54653f1fd342f298957c33dcbf8276146315b20f373a70503f12be6fb65386e49198eb4722e12908d1
|
data/lib/swarm_cli/version.rb
CHANGED
|
@@ -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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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,
|
|
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
|
-
#
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
#
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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:
|
|
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
|
|
526
|
+
logical_path
|
|
511
527
|
end
|
|
512
528
|
|
|
513
|
-
#
|
|
514
|
-
#
|
|
529
|
+
# Identity function - paths are now stored hierarchically
|
|
530
|
+
# Kept for backward compatibility during transition
|
|
515
531
|
#
|
|
516
|
-
# @param disk_path [String]
|
|
517
|
-
# @return [String]
|
|
532
|
+
# @param disk_path [String] Disk path
|
|
533
|
+
# @return [String] Same path (no unflattening)
|
|
518
534
|
def unflatten_path(disk_path)
|
|
519
|
-
disk_path
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
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 +
|
|
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.
|
|
114
|
-
4.
|
|
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
|
|
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
|
|
20
|
-
- `**` - matches
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
194
|
+
return "No matches found for pattern #{search_desc}"
|
|
161
195
|
end
|
|
162
196
|
|
|
163
197
|
result = []
|
|
164
|
-
result << "Memory entries matching
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"}"
|
data/lib/swarm_memory/version.rb
CHANGED
|
@@ -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)
|
data/lib/swarm_sdk/version.rb
CHANGED
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.
|
|
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:
|
|
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.
|
|
310
|
+
rubygems_version: 3.6.9
|
|
311
311
|
specification_version: 4
|
|
312
312
|
summary: Persistent memory system for SwarmSDK agents
|
|
313
313
|
test_files: []
|