swarm_cli 2.0.2 → 2.0.3

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: d5f6285a136ccf9bda4e010daf5062f38d2dc1fbe27ef0747dd7e08a0f794bfc
4
- data.tar.gz: 54400709e283dce60b2422392a45f3ccc56b0876b41ccd919a17b12efbd19413
3
+ metadata.gz: 0f2300451b3b37327ea450e9eebc57db327859aed5b87f7b84a8dc07b7cb1ec2
4
+ data.tar.gz: d6742156018f591a09f396b0e08f63295b6978bfa3a07a3ef76702c791820baa
5
5
  SHA512:
6
- metadata.gz: afbaa1d360f97e20167256ebfa6c6c68d377bac849a295197641443c7a59f306a132a1084a5bf89d8490b9d79dce67f4bb0a1b480642ce68dedeb758af0ef67e
7
- data.tar.gz: b37a53f4855db437e74d2fd6a60245650a41496c603f5529371f5471a4e7d7889bd34f11c5b3764ddc0ae3f97a20777dc9a249da4659c11155a7e5ab7d7f29f8
6
+ metadata.gz: e0a82b80722dab137f89a8249201216c3c112d1d235377e0cb17cc452526dd0c140a020f21b023f20c63ce7e1a3b3c1ae5375d88a8ef6defe0a98a5b6a0e569d
7
+ data.tar.gz: b824bf81f759bf1fd05378cbf6df259298ce6e4118150fc6598b2f3e19f5f17e10dea584a4b91e3348dd176af8423df93e07ab17049b5183747a65f614f4ef97
data/lib/swarm_cli/cli.rb CHANGED
@@ -36,10 +36,15 @@ module SwarmCLI
36
36
  when "migrate"
37
37
  migrate_command(@args[1..])
38
38
  else
39
- $stderr.puts "Unknown command: #{command}"
40
- $stderr.puts
41
- print_help
42
- exit(1)
39
+ # Check if it's an extension command
40
+ if CommandRegistry.registered?(command)
41
+ extension_command(command, @args[1..])
42
+ else
43
+ $stderr.puts "Unknown command: #{command}"
44
+ $stderr.puts
45
+ print_help
46
+ exit(1)
47
+ end
43
48
  end
44
49
  rescue StandardError => e
45
50
  $stderr.puts "Fatal error: #{e.message}"
@@ -123,6 +128,14 @@ module SwarmCLI
123
128
  exit(1)
124
129
  end
125
130
 
131
+ def extension_command(command_name, args)
132
+ # Get extension command class from registry
133
+ command_class = CommandRegistry.get(command_name)
134
+
135
+ # Execute extension command
136
+ command_class.execute(args)
137
+ end
138
+
126
139
  def print_help
127
140
  puts
128
141
  puts "SwarmCLI v#{VERSION} - AI Agent Orchestration"
@@ -132,12 +145,24 @@ module SwarmCLI
132
145
  puts " swarm migrate INPUT_FILE [--output OUTPUT_FILE]"
133
146
  puts " swarm mcp serve CONFIG_FILE"
134
147
  puts " swarm mcp tools [TOOL_NAMES...]"
148
+
149
+ # Show extension commands dynamically
150
+ CommandRegistry.commands.each do |cmd|
151
+ puts " swarm #{cmd} ..."
152
+ end
153
+
135
154
  puts
136
155
  puts "Commands:"
137
156
  puts " run Execute a swarm with AI agents"
138
157
  puts " migrate Migrate Claude Swarm v1 config to SwarmSDK v2 format"
139
158
  puts " mcp serve Start an MCP server exposing swarm lead agent"
140
159
  puts " mcp tools Start an MCP server exposing SwarmSDK tools"
160
+
161
+ # Show extension command descriptions (if registered)
162
+ if CommandRegistry.registered?("memory")
163
+ puts " memory Manage SwarmMemory embeddings"
164
+ end
165
+
141
166
  puts
142
167
  puts "Options:"
143
168
  puts " -p, --prompt PROMPT Task prompt for the swarm"
@@ -159,6 +184,13 @@ module SwarmCLI
159
184
  puts " swarm mcp tools # Expose all SwarmSDK tools"
160
185
  puts " swarm mcp tools Bash Grep Read # Space-separated tools"
161
186
  puts " swarm mcp tools ScratchpadWrite,ScratchpadRead # Comma-separated tools"
187
+
188
+ # Show extension command examples dynamically
189
+ if CommandRegistry.registered?("memory")
190
+ puts " swarm memory setup # Setup embeddings (download model)"
191
+ puts " swarm memory status # Check embedding status"
192
+ end
193
+
162
194
  puts
163
195
  end
164
196
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ # Registry for CLI command extensions
5
+ #
6
+ # Allows gems (like swarm_memory) to register additional CLI commands
7
+ # that integrate seamlessly with the main swarm CLI.
8
+ #
9
+ # @example
10
+ # # In swarm_memory gem
11
+ # SwarmCLI::CommandRegistry.register(:memory, MyMemoryCommand)
12
+ #
13
+ # # User runs:
14
+ # swarm memory status
15
+ #
16
+ # # SwarmCLI routes to MyMemoryCommand.execute(["status"])
17
+ class CommandRegistry
18
+ @extensions = {}
19
+
20
+ class << self
21
+ # Register a command extension
22
+ #
23
+ # @param command_name [Symbol, String] Command name (e.g., :memory)
24
+ # @param command_class [Class] Command class with execute(args) method
25
+ # @return [void]
26
+ #
27
+ # @example
28
+ # CommandRegistry.register(:memory, SwarmMemory::CLI::Commands)
29
+ def register(command_name, command_class)
30
+ @extensions ||= {}
31
+ @extensions[command_name.to_s] = command_class
32
+ end
33
+
34
+ # Get command class by name
35
+ #
36
+ # @param command_name [String] Command name
37
+ # @return [Class, nil] Command class or nil if not found
38
+ def get(command_name)
39
+ @extensions ||= {}
40
+ @extensions[command_name.to_s]
41
+ end
42
+
43
+ # Check if a command is registered
44
+ #
45
+ # @param command_name [String] Command name
46
+ # @return [Boolean] True if command exists
47
+ def registered?(command_name)
48
+ @extensions ||= {}
49
+ @extensions.key?(command_name.to_s)
50
+ end
51
+
52
+ # Get all registered command names
53
+ #
54
+ # @return [Array<String>] Command names
55
+ def commands
56
+ @extensions ||= {}
57
+ @extensions.keys
58
+ end
59
+ end
60
+ end
61
+ end
@@ -264,6 +264,16 @@ module SwarmCLI
264
264
  agent = entry[:agent]
265
265
  @usage_tracker.track_tool_call(tool_call_id: entry[:tool_call_id], tool_name: entry[:tool])
266
266
 
267
+ # Special handling for Think tool - show as thoughts, not as a tool call
268
+ if entry[:tool] == "Think" && entry[:arguments] && entry[:arguments]["thoughts"]
269
+ thoughts = entry[:arguments]["thoughts"]
270
+ thinking = @event_renderer.thinking_text(thoughts, indent: @depth_tracker.get(agent))
271
+ @output.puts thinking unless thinking.empty?
272
+ @output.puts
273
+ # Don't show spinner for Think tool
274
+ return
275
+ end
276
+
267
277
  # Render tool call event
268
278
  @output.puts @event_renderer.tool_call(
269
279
  agent: agent,
@@ -297,6 +307,18 @@ module SwarmCLI
297
307
  agent = entry[:agent]
298
308
  tool_name = entry[:tool] || @usage_tracker.tool_name_for(entry[:tool_call_id])
299
309
 
310
+ # Special handling for Think tool - skip showing result (already shown as thoughts)
311
+ if tool_name == "Think"
312
+ # Don't show anything - thoughts were already displayed in handle_tool_call
313
+ # Start spinner for agent processing
314
+ unless @quiet
315
+ spinner_key = "agent_#{agent}".to_sym
316
+ indent = @depth_tracker.indent(agent)
317
+ @spinner_manager.start(spinner_key, "#{indent}#{agent} is processing...")
318
+ end
319
+ return
320
+ end
321
+
300
322
  # Stop tool spinner with success
301
323
  unless @quiet || tool_name == "TodoWrite"
302
324
  spinner_key = "tool_#{entry[:tool_call_id]}".to_sym
@@ -23,11 +23,23 @@ module SwarmCLI
23
23
  class InteractiveREPL
24
24
  COMMANDS = {
25
25
  "/help" => "Show available commands",
26
- "/clear" => "Clear the screen",
26
+ "/clear" => "Clear the lead agent's conversation context",
27
+ "/tools" => "List the lead agent's available tools",
27
28
  "/history" => "Show conversation history",
29
+ "/defrag" => "Run memory defragmentation workflow (find and link related entries)",
28
30
  "/exit" => "Exit the REPL (or press Ctrl+D)",
29
31
  }.freeze
30
32
 
33
+ # History configuration
34
+ HISTORY_SIZE = 1000
35
+
36
+ class << self
37
+ # Get history file path (can be overridden with SWARM_HISTORY env var)
38
+ def history_file
39
+ ENV["SWARM_HISTORY"] || File.expand_path("~/.swarm/history")
40
+ end
41
+ end
42
+
31
43
  def initialize(swarm:, options:, initial_message: nil)
32
44
  @swarm = swarm
33
45
  @options = options
@@ -37,6 +49,7 @@ module SwarmCLI
37
49
  @validation_warnings_shown = false
38
50
 
39
51
  setup_ui_components
52
+ setup_persistent_history
40
53
 
41
54
  # Create formatter for swarm execution output (interactive mode)
42
55
  @formatter = Formatters::HumanFormatter.new(
@@ -67,6 +80,9 @@ module SwarmCLI
67
80
  display_goodbye
68
81
  display_session_summary
69
82
  exit(130)
83
+ ensure
84
+ # Save history on exit
85
+ save_persistent_history
70
86
  end
71
87
 
72
88
  # Execute a message with Ctrl+C cancellation support
@@ -122,6 +138,61 @@ module SwarmCLI
122
138
  cancelled ? nil : result
123
139
  end
124
140
 
141
+ # Handle slash commands
142
+ # Public for testing
143
+ #
144
+ # @param input [String] Command input (e.g., "/help", "/clear")
145
+ def handle_command(input)
146
+ command = input.split.first.downcase
147
+
148
+ case command
149
+ when "/help"
150
+ display_help
151
+ when "/clear"
152
+ clear_context
153
+ when "/tools"
154
+ list_tools
155
+ when "/history"
156
+ display_history
157
+ when "/defrag"
158
+ defrag_memory
159
+ when "/exit"
160
+ # Break from main loop to trigger session summary
161
+ throw(:exit_repl)
162
+ else
163
+ puts render_error("Unknown command: #{command}")
164
+ puts @colors[:system].call("Type /help for available commands")
165
+ end
166
+ end
167
+
168
+ # Save persistent history to file
169
+ # Public for testing
170
+ #
171
+ # @return [void]
172
+ def save_persistent_history
173
+ history_file = self.class.history_file
174
+ return unless history_file
175
+
176
+ history = Reline::HISTORY.to_a
177
+
178
+ # Limit to configured size
179
+ if HISTORY_SIZE.positive? && history.size > HISTORY_SIZE
180
+ history = history.last(HISTORY_SIZE)
181
+ end
182
+
183
+ # Write with secure permissions (owner read/write only)
184
+ File.open(history_file, "w", 0o600, encoding: Encoding::UTF_8) do |f|
185
+ # Handle multi-line entries by escaping newlines with backslash
186
+ history.each do |entry|
187
+ escaped = entry.scrub.split("\n").join("\\\n")
188
+ f.puts(escaped)
189
+ end
190
+ end
191
+ rescue Errno::EACCES, Errno::ENOENT
192
+ # Can't write history - continue anyway
193
+ nil
194
+ end
195
+
125
196
  private
126
197
 
127
198
  def setup_ui_components
@@ -151,6 +222,9 @@ module SwarmCLI
151
222
  config.add_default_key_binding_by_keymap(:emacs, [9], :fuzzy_complete)
152
223
  config.add_default_key_binding_by_keymap(:vi_insert, [9], :fuzzy_complete)
153
224
 
225
+ # Configure history size
226
+ Reline.core.config.history_size = HISTORY_SIZE
227
+
154
228
  # Setup colors using detached styles for performance
155
229
  @colors = {
156
230
  prompt: @pastel.bright_cyan.bold.detach,
@@ -170,6 +244,33 @@ module SwarmCLI
170
244
  }
171
245
  end
172
246
 
247
+ def setup_persistent_history
248
+ history_file = self.class.history_file
249
+
250
+ # Ensure history directory exists
251
+ FileUtils.mkdir_p(File.dirname(history_file))
252
+
253
+ # Load history from file
254
+ return unless File.exist?(history_file)
255
+
256
+ File.open(history_file, "r:UTF-8") do |f|
257
+ f.each_line do |line|
258
+ line = line.chomp
259
+
260
+ # Handle multi-line entries (backslash continuation)
261
+ if Reline::HISTORY.last&.end_with?("\\")
262
+ Reline::HISTORY.last.delete_suffix!("\\")
263
+ Reline::HISTORY.last << "\n" << line
264
+ else
265
+ Reline::HISTORY << line unless line.empty?
266
+ end
267
+ end
268
+ end
269
+ rescue Errno::ENOENT, Errno::EACCES
270
+ # History file doesn't exist or can't be read - that's OK
271
+ nil
272
+ end
273
+
173
274
  def display_welcome
174
275
  divider = @colors[:divider].call("─" * 60)
175
276
 
@@ -312,26 +413,6 @@ module SwarmCLI
312
413
  end
313
414
  end
314
415
 
315
- def handle_command(input)
316
- command = input.split.first.downcase
317
-
318
- case command
319
- when "/help"
320
- display_help
321
- when "/clear"
322
- system("clear") || system("cls")
323
- display_welcome
324
- when "/history"
325
- display_history
326
- when "/exit"
327
- # Break from main loop to trigger session summary
328
- throw(:exit_repl)
329
- else
330
- puts render_error("Unknown command: #{command}")
331
- puts @colors[:system].call("Type /help for available commands")
332
- end
333
- end
334
-
335
416
  def handle_message(input)
336
417
  # Add to history
337
418
  @conversation_history << { role: "user", content: input }
@@ -442,6 +523,111 @@ module SwarmCLI
442
523
  puts help_box
443
524
  end
444
525
 
526
+ def clear_context
527
+ # Get the lead agent
528
+ lead = @swarm.agent(@swarm.lead_agent)
529
+
530
+ # Clear the agent's conversation history
531
+ lead.reset_messages!
532
+
533
+ # Clear REPL conversation history
534
+ @conversation_history.clear
535
+
536
+ # Display confirmation
537
+ puts ""
538
+ puts @colors[:success].call("✓ Conversation context cleared for #{@swarm.lead_agent}")
539
+ puts @colors[:system].call(" Starting fresh - previous messages removed from context")
540
+ puts ""
541
+ end
542
+
543
+ def list_tools
544
+ # Get the lead agent
545
+ lead = @swarm.agent(@swarm.lead_agent)
546
+
547
+ # Get tools hash (tool_name => tool_instance)
548
+ tools_hash = lead.tools
549
+
550
+ puts ""
551
+ puts @colors[:header].call("Available Tools for #{@swarm.lead_agent}:")
552
+ puts @colors[:divider].call("─" * 60)
553
+ puts ""
554
+
555
+ if tools_hash.empty?
556
+ puts @colors[:system].call("No tools available")
557
+ return
558
+ end
559
+
560
+ # Group tools by category
561
+ memory_tools = []
562
+ standard_tools = []
563
+ delegation_tools = []
564
+ mcp_tools = []
565
+ other_tools = []
566
+
567
+ tools_hash.each_value do |tool|
568
+ tool_name = tool.name
569
+ case tool_name
570
+ when /^Memory/, "LoadSkill"
571
+ memory_tools << tool_name
572
+ when /^DelegateTaskTo/
573
+ delegation_tools << tool_name
574
+ when /^mcp__/
575
+ mcp_tools << tool_name
576
+ when "Read", "Write", "Edit", "MultiEdit", "Bash", "Grep", "Glob",
577
+ "TodoWrite", "Think", "Clock", "WebFetch",
578
+ "ScratchpadWrite", "ScratchpadRead", "ScratchpadList"
579
+ standard_tools << tool_name
580
+ else
581
+ other_tools << tool_name
582
+ end
583
+ end
584
+
585
+ # Display tools by category
586
+ if standard_tools.any?
587
+ puts @colors[:agent_label].call("Standard Tools:")
588
+ standard_tools.sort.each do |name|
589
+ puts @colors[:system].call(" • #{name}")
590
+ end
591
+ puts ""
592
+ end
593
+
594
+ if memory_tools.any?
595
+ puts @colors[:agent_label].call("Memory Tools:")
596
+ memory_tools.sort.each do |name|
597
+ puts @colors[:system].call(" • #{name}")
598
+ end
599
+ puts ""
600
+ end
601
+
602
+ if delegation_tools.any?
603
+ puts @colors[:agent_label].call("Delegation Tools:")
604
+ delegation_tools.sort.each do |name|
605
+ puts @colors[:system].call(" • #{name}")
606
+ end
607
+ puts ""
608
+ end
609
+
610
+ if mcp_tools.any?
611
+ puts @colors[:agent_label].call("MCP Tools:")
612
+ mcp_tools.sort.each do |name|
613
+ puts @colors[:system].call(" • #{name}")
614
+ end
615
+ puts ""
616
+ end
617
+
618
+ if other_tools.any?
619
+ puts @colors[:agent_label].call("Other Tools:")
620
+ other_tools.sort.each do |name|
621
+ puts @colors[:system].call(" • #{name}")
622
+ end
623
+ puts ""
624
+ end
625
+
626
+ puts @colors[:divider].call("─" * 60)
627
+ puts @colors[:system].call("Total: #{tools_hash.size} tools")
628
+ puts ""
629
+ end
630
+
445
631
  def display_history
446
632
  if @conversation_history.empty?
447
633
  puts @colors[:system].call("No conversation history yet")
@@ -474,6 +660,26 @@ module SwarmCLI
474
660
  puts @colors[:divider].call("─" * 60)
475
661
  end
476
662
 
663
+ def defrag_memory
664
+ puts ""
665
+ puts @colors[:header].call("🔧 Memory Defragmentation Workflow")
666
+ puts @colors[:divider].call("─" * 60)
667
+ puts ""
668
+
669
+ # Inject prompt to run find_related then link_related
670
+ prompt = <<~PROMPT.strip
671
+ Run memory defragmentation workflow:
672
+
673
+ 1. First, run MemoryDefrag(action: "find_related") to discover related entries
674
+ 2. Review the results carefully
675
+ 3. Then run MemoryDefrag(action: "link_related", dry_run: false) to create bidirectional links
676
+
677
+ Report what you found and what links were created.
678
+ PROMPT
679
+
680
+ handle_message(prompt)
681
+ end
682
+
477
683
  def display_goodbye
478
684
  puts ""
479
685
  goodbye_text = @colors[:success].call("👋 Goodbye! Thanks for using Swarm CLI")
@@ -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
data/lib/swarm_cli.rb CHANGED
@@ -36,3 +36,10 @@ module SwarmCLI
36
36
  class ConfigurationError < Error; end
37
37
  class ExecutionError < Error; end
38
38
  end
39
+
40
+ # Try to load swarm_memory gem if available (for CLI command extensions)
41
+ begin
42
+ require "swarm_memory"
43
+ rescue LoadError
44
+ # swarm_memory not installed - that's fine, memory commands won't be available
45
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swarm_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
@@ -205,6 +205,20 @@ dependencies:
205
205
  - - "~>"
206
206
  - !ruby/object:Gem::Version
207
207
  version: '2.15'
208
+ - !ruby/object:Gem::Dependency
209
+ name: reverse_markdown
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - "~>"
213
+ - !ruby/object:Gem::Version
214
+ version: 3.0.0
215
+ type: :runtime
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - "~>"
220
+ - !ruby/object:Gem::Version
221
+ version: 3.0.0
208
222
  - !ruby/object:Gem::Dependency
209
223
  name: roo
210
224
  requirement: !ruby/object:Gem::Requirement
@@ -234,6 +248,7 @@ files:
234
248
  - exe/swarm
235
249
  - lib/swarm_cli.rb
236
250
  - lib/swarm_cli/cli.rb
251
+ - lib/swarm_cli/command_registry.rb
237
252
  - lib/swarm_cli/commands/mcp_serve.rb
238
253
  - lib/swarm_cli/commands/mcp_tools.rb
239
254
  - lib/swarm_cli/commands/migrate.rb
@@ -268,7 +283,7 @@ licenses:
268
283
  - MIT
269
284
  metadata:
270
285
  source_code_uri: https://github.com/parruda/claude-swarm
271
- changelog_uri: https://github.com/parruda/claude-swarm/blob/main/CHANGELOG.md
286
+ changelog_uri: https://github.com/parruda/claude-swarm/blob/main/docs/v2/CHANGELOG.swarm_cli.md
272
287
  rdoc_options: []
273
288
  require_paths:
274
289
  - lib