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 +4 -4
- data/lib/swarm_cli/cli.rb +36 -4
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +3 -1
- data/lib/swarm_cli/formatters/human_formatter.rb +24 -0
- data/lib/swarm_cli/interactive_repl.rb +307 -29
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_cli.rb +7 -0
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f2300451b3b37327ea450e9eebc57db327859aed5b87f7b84a8dc07b7cb1ec2
|
|
4
|
+
data.tar.gz: d6742156018f591a09f396b0e08f63295b6978bfa3a07a3ef76702c791820baa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
323
|
+
# Block always returns true = ENTER always submits
|
|
324
|
+
input = Reline.readmultiline(prompt_indicator, true) { |_lines| true }
|
|
165
325
|
|
|
166
|
-
return if
|
|
326
|
+
return if input.nil? # Ctrl+D returns nil
|
|
167
327
|
|
|
168
|
-
#
|
|
169
|
-
|
|
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
|
|
283
|
-
result =
|
|
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(" •
|
|
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")
|
data/lib/swarm_cli/version.rb
CHANGED
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.
|
|
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
|