swarm_memory 2.0.0 → 2.1.1

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: 38d427a70039bc09a7a1bbad5a0f2c9f28e46d80a954587ab27f04ad33c66e2f
4
+ data.tar.gz: 3942488b1873520a984493c6270085b0f6f2e0e01560bea349903463d01bda11
5
5
  SHA512:
6
- metadata.gz: 91b645763ed45723d82080fb2321ca5e9c0d61310c3f3907843c9086980f5b27e0176315b2ac399f837191c5559b6e36563e2f71bb18adaf2a93a957f186a2e9
7
- data.tar.gz: 9b019e85c71c3f7c9f1c1ac5358c53566b5c5934fb1e983087e9c6ebdd81c79b1e32c100d762161bda4aff2227c2be24bfa3d9f0784edc7cbd227a9a21d724af
6
+ metadata.gz: 945735c0d60be12edc1452ea84059f6a3d6aa68966e687a81d2c8ccbd2492c5b2bdd9099fc4a14c40aaabd1d275cec0a80b35be8c9f5354d217467136493ec57
7
+ data.tar.gz: f5b0680131c7d38ff229da49031628356c44c4edcb90685b7489e2bc2b30a6d361babf55d3e995d5f28acc62f56b28deaa03829d512f95550dd198b192179df0
data/lib/claude_swarm.rb CHANGED
@@ -30,13 +30,12 @@ require "thor"
30
30
  require "zeitwerk"
31
31
  loader = Zeitwerk::Loader.new
32
32
  loader.tag = "claude_swarm"
33
- loader.push_dir("#{__dir__}/claude_swarm", namespace: ClaudeSwarm)
33
+
34
34
  loader.ignore("#{__dir__}/claude_swarm/templates")
35
35
  loader.inflector.inflect(
36
36
  "cli" => "CLI",
37
37
  "openai" => "OpenAI",
38
38
  )
39
- loader.setup
40
39
 
41
40
  module ClaudeSwarm
42
41
  class Error < StandardError; end
@@ -67,3 +66,6 @@ module ClaudeSwarm
67
66
  end
68
67
  end
69
68
  end
69
+
70
+ loader.push_dir("#{__dir__}/claude_swarm", namespace: ClaudeSwarm)
71
+ loader.setup
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmCLI
4
- VERSION = "2.0.2"
4
+ VERSION = "2.1.0"
5
5
  end
data/lib/swarm_cli.rb CHANGED
@@ -18,8 +18,7 @@ require "tty/tree"
18
18
 
19
19
  require "swarm_sdk"
20
20
 
21
- module SwarmCLI
22
- end
21
+ require_relative "swarm_cli/version"
23
22
 
24
23
  require "zeitwerk"
25
24
  loader = Zeitwerk::Loader.new
@@ -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` (1000 words covering everything)
111
+
112
+ Create multiple linked memories with FULL details in each:
113
+ ✅ `concept/payment/processing-flow.md` (250 words) (complete flow with all steps) → related: ["concept/payment/validation.md"]
114
+ ✅ `concept/payment/validation.md` (250 words) (all validation rules with specifics) → related: ["concept/payment/processing-flow.md", "concept/payment/error-handling.md"]
115
+ ✅ `concept/payment/error-handling.md` (250 words) (all error codes and responses) → related: ["concept/payment/validation.md"]
116
+ ✅ `concept/payment/security.md` (250 words) (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.