swarm_cli 2.0.1 → 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: f57ce5f511e43f8c520bf787d993cdcff34f3152870d496f72b5bbdee962c1a5
4
- data.tar.gz: e21157d97dfce0fc71806683b6cb755c0e5ce9901c43c71764aab2b398d23fb1
3
+ metadata.gz: 0f2300451b3b37327ea450e9eebc57db327859aed5b87f7b84a8dc07b7cb1ec2
4
+ data.tar.gz: d6742156018f591a09f396b0e08f63295b6978bfa3a07a3ef76702c791820baa
5
5
  SHA512:
6
- metadata.gz: e8702078d6573711ebe3c08c74571c0ce3d6b1de19810fa324cf6a38122573c7268c906b35e9f3446eb812dbad7d06ce4b61a01e24ff4c2ba5d56244f2b659d0
7
- data.tar.gz: 252d1ee2ad3acb39f4302a558ec8633a5eba854522b6924362ecbee4db9f0dfdfe92020a531333d615f1402879b3d2ea8ed9ee4ee140ec668c59a6ad5254f20e
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
@@ -14,7 +14,9 @@ module SwarmCLI
14
14
 
15
15
  def initialize(options)
16
16
  @options = options
17
- @scratchpad = SwarmSDK::Scratchpad.new
17
+ # Create scratchpad with persistence for MCP server
18
+ scratchpad_path = File.join(Dir.pwd, ".swarm", "scratchpad.json")
19
+ @scratchpad = SwarmSDK::Scratchpad.new(persist_to: scratchpad_path)
18
20
  end
19
21
 
20
22
  def execute
@@ -10,6 +10,8 @@ module SwarmCLI
10
10
  # - :non_interactive - Full headers, task prompt, complete summary (for single execution)
11
11
  # - :interactive - Minimal output for REPL (headers shown in welcome screen)
12
12
  class HumanFormatter
13
+ attr_reader :spinner_manager
14
+
13
15
  def initialize(output: $stdout, quiet: false, truncate: false, verbose: false, mode: :non_interactive)
14
16
  @output = output
15
17
  @quiet = quiet
@@ -262,6 +264,16 @@ module SwarmCLI
262
264
  agent = entry[:agent]
263
265
  @usage_tracker.track_tool_call(tool_call_id: entry[:tool_call_id], tool_name: entry[:tool])
264
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
+
265
277
  # Render tool call event
266
278
  @output.puts @event_renderer.tool_call(
267
279
  agent: agent,
@@ -295,6 +307,18 @@ module SwarmCLI
295
307
  agent = entry[:agent]
296
308
  tool_name = entry[:tool] || @usage_tracker.tool_name_for(entry[:tool_call_id])
297
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
+
298
322
  # Stop tool spinner with success
299
323
  unless @quiet || tool_name == "TodoWrite"
300
324
  spinner_key = "tool_#{entry[:tool_call_id]}".to_sym
@@ -5,6 +5,8 @@ require "tty-spinner"
5
5
  require "tty-markdown"
6
6
  require "tty-box"
7
7
  require "pastel"
8
+ require "async"
9
+ require "async/condition"
8
10
 
9
11
  module SwarmCLI
10
12
  # InteractiveREPL provides a professional, interactive terminal interface
@@ -21,11 +23,23 @@ module SwarmCLI
21
23
  class InteractiveREPL
22
24
  COMMANDS = {
23
25
  "/help" => "Show available commands",
24
- "/clear" => "Clear the screen",
26
+ "/clear" => "Clear the lead agent's conversation context",
27
+ "/tools" => "List the lead agent's available tools",
25
28
  "/history" => "Show conversation history",
29
+ "/defrag" => "Run memory defragmentation workflow (find and link related entries)",
26
30
  "/exit" => "Exit the REPL (or press Ctrl+D)",
27
31
  }.freeze
28
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
+
29
43
  def initialize(swarm:, options:, initial_message: nil)
30
44
  @swarm = swarm
31
45
  @options = options
@@ -35,6 +49,7 @@ module SwarmCLI
35
49
  @validation_warnings_shown = false
36
50
 
37
51
  setup_ui_components
52
+ setup_persistent_history
38
53
 
39
54
  # Create formatter for swarm execution output (interactive mode)
40
55
  @formatter = Formatters::HumanFormatter.new(
@@ -65,6 +80,117 @@ module SwarmCLI
65
80
  display_goodbye
66
81
  display_session_summary
67
82
  exit(130)
83
+ ensure
84
+ # Save history on exit
85
+ save_persistent_history
86
+ end
87
+
88
+ # Execute a message with Ctrl+C cancellation support
89
+ # Public for testing
90
+ #
91
+ # @param input [String] User input to execute
92
+ # @return [SwarmSDK::Result, nil] Result or nil if cancelled
93
+ def execute_with_cancellation(input, &log_callback)
94
+ cancelled = false
95
+ result = nil
96
+
97
+ # Execute in Async block to enable Ctrl+C cancellation
98
+ Async do |task|
99
+ # Use Async::Condition for trap-safe cancellation
100
+ # (Condition#signal uses Thread::Queue which is safe from trap context)
101
+ cancel_condition = Async::Condition.new
102
+
103
+ # Install trap ONLY during execution
104
+ # When Ctrl+C is pressed, signal the condition instead of calling task.stop
105
+ old_trap = trap("INT") do
106
+ cancel_condition.signal(:cancel)
107
+ end
108
+
109
+ begin
110
+ # Execute swarm in async task
111
+ llm_task = task.async do
112
+ @swarm.execute(input, &log_callback)
113
+ end
114
+
115
+ # Monitor task - watches for cancellation signal
116
+ # Must be created AFTER llm_task so it can reference it
117
+ monitor_task = task.async do
118
+ if cancel_condition.wait == :cancel
119
+ cancelled = true
120
+ llm_task.stop
121
+ end
122
+ end
123
+
124
+ result = llm_task.wait
125
+ rescue Async::Stop
126
+ # Task was stopped by Ctrl+C
127
+ cancelled = true
128
+ ensure
129
+ # Clean up monitor task
130
+ monitor_task&.stop if monitor_task&.alive?
131
+
132
+ # CRITICAL: Restore old trap when done
133
+ # This ensures Ctrl+C at the prompt still exits the REPL
134
+ trap("INT", old_trap)
135
+ end
136
+ end.wait
137
+
138
+ cancelled ? nil : result
139
+ end
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
68
194
  end
69
195
 
70
196
  private
@@ -96,6 +222,9 @@ module SwarmCLI
96
222
  config.add_default_key_binding_by_keymap(:emacs, [9], :fuzzy_complete)
97
223
  config.add_default_key_binding_by_keymap(:vi_insert, [9], :fuzzy_complete)
98
224
 
225
+ # Configure history size
226
+ Reline.core.config.history_size = HISTORY_SIZE
227
+
99
228
  # Setup colors using detached styles for performance
100
229
  @colors = {
101
230
  prompt: @pastel.bright_cyan.bold.detach,
@@ -115,6 +244,33 @@ module SwarmCLI
115
244
  }
116
245
  end
117
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
+
118
274
  def display_welcome
119
275
  divider = @colors[:divider].call("─" * 60)
120
276
 
@@ -127,6 +283,7 @@ module SwarmCLI
127
283
  puts @colors[:system].call("Lead Agent: #{@swarm.lead_agent}")
128
284
  puts ""
129
285
  puts @colors[:system].call("Type your message and press Enter to submit")
286
+ puts @colors[:system].call("Press Option+Enter (or ESC then Enter) for multi-line input")
130
287
  puts @colors[:system].call("Type #{@colors[:code].call("/help")} for commands or #{@colors[:code].call("/exit")} to quit")
131
288
  puts ""
132
289
  puts divider
@@ -159,14 +316,17 @@ module SwarmCLI
159
316
  # Build the prompt indicator with colors
160
317
  prompt_indicator = build_prompt_indicator
161
318
 
162
- # Use Reline for flicker-free input (same as IRB)
319
+ # Use Reline.readmultiline for multi-line input support
320
+ # - Option+ENTER (or ESC+ENTER): Adds a newline, continues editing
321
+ # - Regular ENTER: Always submits immediately
163
322
  # Second parameter true = add to history for arrow up/down
164
- line = Reline.readline(prompt_indicator, true)
323
+ # Block always returns true = ENTER always submits
324
+ input = Reline.readmultiline(prompt_indicator, true) { |_lines| true }
165
325
 
166
- return if line.nil? # Ctrl+D returns nil
326
+ return if input.nil? # Ctrl+D returns nil
167
327
 
168
- # Reline doesn't include newline, just strip whitespace
169
- line.strip
328
+ # Strip whitespace from the complete input
329
+ input.strip
170
330
  end
171
331
 
172
332
  def display_prompt_stats
@@ -253,34 +413,14 @@ module SwarmCLI
253
413
  end
254
414
  end
255
415
 
256
- def handle_command(input)
257
- command = input.split.first.downcase
258
-
259
- case command
260
- when "/help"
261
- display_help
262
- when "/clear"
263
- system("clear") || system("cls")
264
- display_welcome
265
- when "/history"
266
- display_history
267
- when "/exit"
268
- # Break from main loop to trigger session summary
269
- throw(:exit_repl)
270
- else
271
- puts render_error("Unknown command: #{command}")
272
- puts @colors[:system].call("Type /help for available commands")
273
- end
274
- end
275
-
276
416
  def handle_message(input)
277
417
  # Add to history
278
418
  @conversation_history << { role: "user", content: input }
279
419
 
280
420
  puts ""
281
421
 
282
- # Execute swarm with logging through formatter
283
- result = @swarm.execute(input) do |log_entry|
422
+ # Execute with cancellation support
423
+ result = execute_with_cancellation(input) do |log_entry|
284
424
  # Skip model warnings - already emitted before first prompt
285
425
  next if log_entry[:type] == "model_lookup_warning"
286
426
 
@@ -292,6 +432,17 @@ module SwarmCLI
292
432
  end
293
433
  end
294
434
 
435
+ # Handle cancellation (result is nil when cancelled)
436
+ if result.nil?
437
+ # Stop all active spinners
438
+ @formatter.spinner_manager.stop_all
439
+
440
+ puts ""
441
+ puts @colors[:warning].call("✗ Request cancelled by user")
442
+ puts ""
443
+ return
444
+ end
445
+
295
446
  # Check for errors
296
447
  if result.failure?
297
448
  @formatter.on_error(error: result.error, duration: result.duration)
@@ -353,7 +504,9 @@ module SwarmCLI
353
504
  end,
354
505
  "",
355
506
  @colors[:system].call("Input Tips:"),
356
- @colors[:system].call(" • Type your message and press Enter to submit"),
507
+ @colors[:system].call(" • Press Enter to submit your message"),
508
+ @colors[:system].call(" • Press Option+Enter (or ESC then Enter) for multi-line input"),
509
+ @colors[:system].call(" • Press Ctrl+C to cancel an ongoing request"),
357
510
  @colors[:system].call(" • Press Ctrl+D to exit"),
358
511
  @colors[:system].call(" • Use arrow keys for history and editing"),
359
512
  @colors[:system].call(" • Type / for commands or @ for file paths"),
@@ -370,6 +523,111 @@ module SwarmCLI
370
523
  puts help_box
371
524
  end
372
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
+
373
631
  def display_history
374
632
  if @conversation_history.empty?
375
633
  puts @colors[:system].call("No conversation history yet")
@@ -402,6 +660,26 @@ module SwarmCLI
402
660
  puts @colors[:divider].call("─" * 60)
403
661
  end
404
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
+
405
683
  def display_goodbye
406
684
  puts ""
407
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.1"
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.1
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