swarm_memory 2.1.5 → 2.1.6

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.
Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. metadata +5 -184
  4. data/lib/claude_swarm/base_executor.rb +0 -133
  5. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  6. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  7. data/lib/claude_swarm/cli.rb +0 -697
  8. data/lib/claude_swarm/commands/ps.rb +0 -215
  9. data/lib/claude_swarm/commands/show.rb +0 -139
  10. data/lib/claude_swarm/configuration.rb +0 -373
  11. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  12. data/lib/claude_swarm/json_handler.rb +0 -91
  13. data/lib/claude_swarm/mcp_generator.rb +0 -230
  14. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  15. data/lib/claude_swarm/openai/executor.rb +0 -256
  16. data/lib/claude_swarm/openai/responses.rb +0 -319
  17. data/lib/claude_swarm/orchestrator.rb +0 -878
  18. data/lib/claude_swarm/process_tracker.rb +0 -78
  19. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  20. data/lib/claude_swarm/session_path.rb +0 -42
  21. data/lib/claude_swarm/settings_generator.rb +0 -77
  22. data/lib/claude_swarm/system_utils.rb +0 -46
  23. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  24. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  25. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  27. data/lib/claude_swarm/version.rb +0 -5
  28. data/lib/claude_swarm/worktree_manager.rb +0 -475
  29. data/lib/claude_swarm/yaml_loader.rb +0 -22
  30. data/lib/claude_swarm.rb +0 -67
  31. data/lib/swarm_cli/cli.rb +0 -201
  32. data/lib/swarm_cli/command_registry.rb +0 -61
  33. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  34. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  35. data/lib/swarm_cli/commands/migrate.rb +0 -55
  36. data/lib/swarm_cli/commands/run.rb +0 -173
  37. data/lib/swarm_cli/config_loader.rb +0 -98
  38. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  39. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  40. data/lib/swarm_cli/interactive_repl.rb +0 -924
  41. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  42. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  43. data/lib/swarm_cli/migrate_options.rb +0 -54
  44. data/lib/swarm_cli/migrator.rb +0 -132
  45. data/lib/swarm_cli/options.rb +0 -151
  46. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  47. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  48. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  49. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  50. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  51. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  52. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  53. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  54. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  55. data/lib/swarm_cli/ui/icons.rb +0 -36
  56. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  57. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  58. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  59. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  60. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  61. data/lib/swarm_cli/version.rb +0 -5
  62. data/lib/swarm_cli.rb +0 -46
  63. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  64. data/lib/swarm_sdk/agent/builder.rb +0 -552
  65. data/lib/swarm_sdk/agent/chat.rb +0 -774
  66. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  67. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  68. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  69. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  70. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  71. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  72. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  73. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  75. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  76. data/lib/swarm_sdk/agent/context.rb +0 -116
  77. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  78. data/lib/swarm_sdk/agent/definition.rb +0 -477
  79. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  80. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  81. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  82. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  83. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  84. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  85. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  86. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  87. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  88. data/lib/swarm_sdk/configuration.rb +0 -135
  89. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  90. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  91. data/lib/swarm_sdk/context_compactor.rb +0 -335
  92. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  93. data/lib/swarm_sdk/context_management/context.rb +0 -328
  94. data/lib/swarm_sdk/defaults.rb +0 -196
  95. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  96. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  97. data/lib/swarm_sdk/hooks/context.rb +0 -197
  98. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  99. data/lib/swarm_sdk/hooks/error.rb +0 -29
  100. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  101. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  102. data/lib/swarm_sdk/hooks/result.rb +0 -150
  103. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  104. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  105. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  106. data/lib/swarm_sdk/log_collector.rb +0 -227
  107. data/lib/swarm_sdk/log_stream.rb +0 -127
  108. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  109. data/lib/swarm_sdk/model_aliases.json +0 -8
  110. data/lib/swarm_sdk/models.json +0 -1
  111. data/lib/swarm_sdk/models.rb +0 -120
  112. data/lib/swarm_sdk/node_context.rb +0 -245
  113. data/lib/swarm_sdk/observer/builder.rb +0 -81
  114. data/lib/swarm_sdk/observer/config.rb +0 -45
  115. data/lib/swarm_sdk/observer/manager.rb +0 -236
  116. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  117. data/lib/swarm_sdk/permissions/config.rb +0 -239
  118. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  119. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  120. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  121. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  122. data/lib/swarm_sdk/plugin.rb +0 -309
  123. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  124. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  125. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  126. data/lib/swarm_sdk/restore_result.rb +0 -65
  127. data/lib/swarm_sdk/result.rb +0 -123
  128. data/lib/swarm_sdk/snapshot.rb +0 -156
  129. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  130. data/lib/swarm_sdk/state_restorer.rb +0 -476
  131. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  132. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  133. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  134. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  135. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  136. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  141. data/lib/swarm_sdk/swarm.rb +0 -717
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/bash.rb +0 -282
  145. data/lib/swarm_sdk/tools/clock.rb +0 -44
  146. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  147. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  148. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  149. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  150. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  151. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  152. data/lib/swarm_sdk/tools/edit.rb +0 -145
  153. data/lib/swarm_sdk/tools/glob.rb +0 -166
  154. data/lib/swarm_sdk/tools/grep.rb +0 -235
  155. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  156. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  157. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -98
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/utils.rb +0 -68
  174. data/lib/swarm_sdk/validation_result.rb +0 -33
  175. data/lib/swarm_sdk/version.rb +0 -5
  176. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  177. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  178. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  179. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  180. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  181. data/lib/swarm_sdk/workflow.rb +0 -554
  182. data/lib/swarm_sdk.rb +0 -524
@@ -1,924 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "reline"
4
- require "tty-spinner"
5
- require "tty-markdown"
6
- require "tty-box"
7
- require "pastel"
8
- require "async"
9
- require "async/condition"
10
-
11
- module SwarmCLI
12
- # InteractiveREPL provides a professional, interactive terminal interface
13
- # for conversing with SwarmSDK agents.
14
- #
15
- # Features:
16
- # - Multiline input with intuitive submission (Enter on empty line or Ctrl+D)
17
- # - Beautiful Markdown rendering for agent responses
18
- # - Progress indicators during processing
19
- # - Command system (/help, /exit, /clear, etc.)
20
- # - Conversation history with context preservation
21
- # - Professional styling with Pastel and TTY tools
22
- #
23
- class InteractiveREPL
24
- COMMANDS = {
25
- "/help" => "Show available commands",
26
- "/clear" => "Clear the lead agent's conversation context",
27
- "/tools" => "List the lead agent's available tools",
28
- "/history" => "Show conversation history",
29
- "/defrag" => "Run memory defragmentation workflow (find and link related entries)",
30
- "/exit" => "Exit the REPL (or press Ctrl+D)",
31
- }.freeze
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
-
43
- def initialize(swarm:, options:, initial_message: nil)
44
- @swarm = swarm
45
- @options = options
46
- @initial_message = initial_message
47
- @conversation_history = []
48
- @session_results = [] # Accumulate all results for session summary
49
- @validation_warnings_shown = false
50
-
51
- setup_ui_components
52
- setup_persistent_history
53
-
54
- # Create formatter for swarm execution output (interactive mode)
55
- @formatter = Formatters::HumanFormatter.new(
56
- output: $stdout,
57
- quiet: options.quiet?,
58
- truncate: options.truncate?,
59
- verbose: options.verbose?,
60
- mode: :interactive,
61
- )
62
- end
63
-
64
- def run
65
- display_welcome
66
-
67
- # Emit validation warnings before first prompt
68
- emit_validation_warnings_before_prompt
69
-
70
- # Send initial message if provided
71
- if @initial_message && !@initial_message.empty?
72
- handle_message(@initial_message)
73
- end
74
-
75
- main_loop
76
- display_goodbye
77
- display_session_summary
78
- rescue Interrupt
79
- puts "\n"
80
- display_goodbye
81
- display_session_summary
82
- exit(130)
83
- ensure
84
- # Defensive: ensure all spinners are stopped on exit
85
- @formatter&.spinner_manager&.stop_all
86
-
87
- # Save history on exit
88
- save_persistent_history
89
- end
90
-
91
- # Execute a message with Ctrl+C cancellation support
92
- # Public for testing
93
- #
94
- # @param input [String] User input to execute
95
- # @return [SwarmSDK::Result, nil] Result or nil if cancelled
96
- def execute_with_cancellation(input, &log_callback)
97
- cancelled = false
98
- result = nil
99
-
100
- # Execute in Async block to enable Ctrl+C cancellation
101
- Async do |task|
102
- # Use Async::Condition for trap-safe cancellation
103
- # (Condition#signal uses Thread::Queue which is safe from trap context)
104
- cancel_condition = Async::Condition.new
105
-
106
- # Install trap ONLY during execution
107
- # When Ctrl+C is pressed, signal the condition instead of calling task.stop
108
- old_trap = trap("INT") do
109
- cancel_condition.signal(:cancel)
110
- end
111
-
112
- begin
113
- # Execute swarm in async task
114
- llm_task = task.async do
115
- @swarm.execute(input, &log_callback)
116
- end
117
-
118
- # Monitor task - watches for cancellation signal
119
- # Must be created AFTER llm_task so it can reference it
120
- monitor_task = task.async do
121
- if cancel_condition.wait == :cancel
122
- cancelled = true
123
- llm_task.stop
124
- end
125
- end
126
-
127
- result = llm_task.wait
128
- rescue Async::Stop
129
- # Task was stopped by Ctrl+C
130
- cancelled = true
131
- ensure
132
- # Clean up monitor task
133
- monitor_task&.stop if monitor_task&.alive?
134
-
135
- # CRITICAL: Restore old trap when done
136
- # This ensures Ctrl+C at the prompt still exits the REPL
137
- trap("INT", old_trap)
138
- end
139
- end.wait
140
-
141
- cancelled ? nil : result
142
- end
143
-
144
- # Handle slash commands
145
- # Public for testing
146
- #
147
- # @param input [String] Command input (e.g., "/help", "/clear")
148
- def handle_command(input)
149
- command = input.split.first.downcase
150
-
151
- case command
152
- when "/help"
153
- display_help
154
- when "/clear"
155
- clear_context
156
- when "/tools"
157
- list_tools
158
- when "/history"
159
- display_history
160
- when "/defrag"
161
- defrag_memory
162
- when "/exit"
163
- # Break from main loop to trigger session summary
164
- throw(:exit_repl)
165
- else
166
- puts render_error("Unknown command: #{command}")
167
- puts @colors[:system].call("Type /help for available commands")
168
- end
169
- end
170
-
171
- # Save persistent history to file
172
- # Public for testing
173
- #
174
- # @return [void]
175
- def save_persistent_history
176
- history_file = self.class.history_file
177
- return unless history_file
178
-
179
- history = Reline::HISTORY.to_a
180
-
181
- # Limit to configured size
182
- if HISTORY_SIZE.positive? && history.size > HISTORY_SIZE
183
- history = history.last(HISTORY_SIZE)
184
- end
185
-
186
- # Write with secure permissions (owner read/write only)
187
- File.open(history_file, "w", 0o600, encoding: Encoding::UTF_8) do |f|
188
- # Handle multi-line entries by escaping newlines with backslash
189
- history.each do |entry|
190
- escaped = entry.scrub.split("\n").join("\\\n")
191
- f.puts(escaped)
192
- end
193
- end
194
- rescue Errno::EACCES, Errno::ENOENT
195
- # Can't write history - continue anyway
196
- nil
197
- end
198
-
199
- private
200
-
201
- def setup_ui_components
202
- @pastel = Pastel.new(enabled: $stdout.tty?)
203
-
204
- # Configure Reline for smooth, flicker-free input (like IRB)
205
- Reline.output = $stdout
206
- Reline.input = $stdin
207
-
208
- # Configure tab completion UI colors (Ruby 3.1+)
209
- configure_completion_ui
210
-
211
- # Enable automatic completions (show as you type)
212
- Reline.autocompletion = true
213
-
214
- # Configure word break characters
215
- Reline.completer_word_break_characters = " \t\n,;|&"
216
-
217
- # Disable default autocomplete (uses start_with? filtering)
218
- Reline.add_dialog_proc(:autocomplete, nil, nil)
219
-
220
- # Add custom fuzzy completion dialog (bypasses Reline's filtering)
221
- setup_fuzzy_completion
222
-
223
- # Rebind Tab to invoke our custom dialog (not the default :complete method)
224
- config = Reline.core.config
225
- config.add_default_key_binding_by_keymap(:emacs, [9], :fuzzy_complete)
226
- config.add_default_key_binding_by_keymap(:vi_insert, [9], :fuzzy_complete)
227
-
228
- # Configure history size
229
- Reline.core.config.history_size = HISTORY_SIZE
230
-
231
- # Setup colors using detached styles for performance
232
- @colors = {
233
- prompt: @pastel.bright_cyan.bold.detach,
234
- user_input: @pastel.white.detach,
235
- agent_text: @pastel.bright_white.detach,
236
- agent_label: @pastel.bright_blue.bold.detach,
237
- success: @pastel.bright_green.detach,
238
- success_icon: @pastel.bright_green.bold.detach,
239
- error: @pastel.bright_red.detach,
240
- error_icon: @pastel.bright_red.bold.detach,
241
- warning: @pastel.bright_yellow.detach,
242
- system: @pastel.dim.detach,
243
- system_bracket: @pastel.bright_black.detach,
244
- divider: @pastel.bright_black.detach,
245
- header: @pastel.bright_cyan.bold.detach,
246
- code: @pastel.bright_magenta.detach,
247
- }
248
- end
249
-
250
- def setup_persistent_history
251
- history_file = self.class.history_file
252
-
253
- # Ensure history directory exists
254
- FileUtils.mkdir_p(File.dirname(history_file))
255
-
256
- # Load history from file
257
- return unless File.exist?(history_file)
258
-
259
- File.open(history_file, "r:UTF-8") do |f|
260
- f.each_line do |line|
261
- line = line.chomp
262
-
263
- # Handle multi-line entries (backslash continuation)
264
- if Reline::HISTORY.last&.end_with?("\\")
265
- Reline::HISTORY.last.delete_suffix!("\\")
266
- Reline::HISTORY.last << "\n" << line
267
- else
268
- Reline::HISTORY << line unless line.empty?
269
- end
270
- end
271
- end
272
- rescue Errno::ENOENT, Errno::EACCES
273
- # History file doesn't exist or can't be read - that's OK
274
- nil
275
- end
276
-
277
- def display_welcome
278
- divider = @colors[:divider].call("─" * 60)
279
-
280
- puts ""
281
- puts divider
282
- puts @colors[:header].call("🚀 Swarm CLI Interactive REPL")
283
- puts divider
284
- puts ""
285
- puts @colors[:agent_text].call("Swarm: #{@swarm.name}")
286
- puts @colors[:system].call("Lead Agent: #{@swarm.lead_agent}")
287
- puts ""
288
- puts @colors[:system].call("Type your message and press Enter to submit")
289
- puts @colors[:system].call("Press Option+Enter (or ESC then Enter) for multi-line input")
290
- puts @colors[:system].call("Type #{@colors[:code].call("/help")} for commands or #{@colors[:code].call("/exit")} to quit")
291
- puts ""
292
- puts divider
293
- puts ""
294
- end
295
-
296
- def main_loop
297
- catch(:exit_repl) do
298
- loop do
299
- input = read_user_input
300
-
301
- break if input.nil? # Ctrl+D pressed
302
- next if input.strip.empty?
303
-
304
- if input.start_with?("/")
305
- handle_command(input.strip)
306
- else
307
- handle_message(input)
308
- end
309
-
310
- puts "" # Spacing between interactions
311
- end
312
- end
313
- end
314
-
315
- def read_user_input
316
- # Display stats separately (they scroll up naturally)
317
- display_prompt_stats
318
-
319
- # Build the prompt indicator with colors
320
- prompt_indicator = build_prompt_indicator
321
-
322
- # Use Reline.readmultiline for multi-line input support
323
- # - Option+ENTER (or ESC+ENTER): Adds a newline, continues editing
324
- # - Regular ENTER: Always submits immediately
325
- # Second parameter true = add to history for arrow up/down
326
- # Block always returns true = ENTER always submits
327
- input = Reline.readmultiline(prompt_indicator, true) { |_lines| true }
328
-
329
- return if input.nil? # Ctrl+D returns nil
330
-
331
- # Strip whitespace from the complete input
332
- input.strip
333
- end
334
-
335
- def display_prompt_stats
336
- # Only show stats if we have conversation history
337
- stats = build_prompt_stats
338
- puts stats if stats && !stats.empty?
339
- end
340
-
341
- def build_prompt_indicator
342
- # Reline supports ANSI colors without flickering!
343
- # Use your beautiful colored prompt
344
- @pastel.bright_cyan("You") +
345
- @pastel.bright_black(" ❯ ")
346
- end
347
-
348
- def build_prompt_stats
349
- return "" if @conversation_history.empty?
350
-
351
- parts = []
352
-
353
- # Agent name
354
- parts << @colors[:agent_label].call(@swarm.lead_agent.to_s)
355
-
356
- # Message count (user messages only)
357
- msg_count = @conversation_history.count { |entry| entry[:role] == "user" }
358
- parts << "#{msg_count} #{msg_count == 1 ? "msg" : "msgs"}"
359
-
360
- # Get last result stats if available
361
- if @last_result
362
- # Token count
363
- tokens = @last_result.total_tokens
364
- if tokens > 0
365
- formatted_tokens = format_number(tokens)
366
- parts << "#{formatted_tokens} tokens"
367
- end
368
-
369
- # Cost
370
- cost = @last_result.total_cost
371
- if cost > 0
372
- formatted_cost = format_cost_value(cost)
373
- parts << formatted_cost
374
- end
375
-
376
- # Context percentage (from last log entry with usage info)
377
- if @last_context_percentage
378
- color_method = context_percentage_color(@last_context_percentage)
379
- colored_pct = @pastel.public_send(color_method, @last_context_percentage)
380
- parts << "#{colored_pct} context"
381
- end
382
- end
383
-
384
- "[#{parts.join(" • ")}]"
385
- end
386
-
387
- def format_number(num)
388
- if num >= 1_000_000
389
- "#{(num / 1_000_000.0).round(1)}M"
390
- elsif num >= 1_000
391
- "#{(num / 1_000.0).round(1)}K"
392
- else
393
- num.to_s
394
- end
395
- end
396
-
397
- def format_cost_value(cost)
398
- if cost < 0.01
399
- "$#{format("%.4f", cost)}"
400
- elsif cost < 1.0
401
- "$#{format("%.3f", cost)}"
402
- else
403
- "$#{format("%.2f", cost)}"
404
- end
405
- end
406
-
407
- def context_percentage_color(percentage_string)
408
- percentage = percentage_string.to_s.gsub("%", "").to_f
409
-
410
- if percentage < 50
411
- :green
412
- elsif percentage < 80
413
- :yellow
414
- else
415
- :red
416
- end
417
- end
418
-
419
- def handle_message(input)
420
- # Add to history
421
- @conversation_history << { role: "user", content: input }
422
-
423
- puts ""
424
-
425
- # Execute with cancellation support
426
- result = execute_with_cancellation(input) do |log_entry|
427
- # Skip model warnings - already emitted before first prompt
428
- next if log_entry[:type] == "model_lookup_warning"
429
-
430
- @formatter.on_log(log_entry)
431
-
432
- # Track context percentage from usage info
433
- if log_entry[:usage] && log_entry[:usage][:tokens_used_percentage]
434
- @last_context_percentage = log_entry[:usage][:tokens_used_percentage]
435
- end
436
- end
437
-
438
- # CRITICAL: Stop all spinners after execution completes
439
- # This ensures spinner doesn't interfere with error/success display or REPL prompt
440
- @formatter.spinner_manager.stop_all
441
-
442
- # Handle cancellation (result is nil when cancelled)
443
- if result.nil?
444
- puts ""
445
- puts @colors[:warning].call("✗ Request cancelled by user")
446
- puts ""
447
- return
448
- end
449
-
450
- # Check for errors
451
- if result.failure?
452
- @formatter.on_error(error: result.error, duration: result.duration)
453
- return
454
- end
455
-
456
- # Display success through formatter (minimal in interactive mode)
457
- @formatter.on_success(result: result)
458
-
459
- # Store result for prompt stats and session summary
460
- @last_result = result
461
- @session_results << result
462
-
463
- # Add response to history
464
- @conversation_history << { role: "agent", content: result.content }
465
- rescue StandardError => e
466
- # Defensive: ensure spinners are stopped on exception
467
- @formatter.spinner_manager.stop_all
468
- @formatter.on_error(error: e)
469
- end
470
-
471
- def emit_validation_warnings_before_prompt
472
- # Setup temporary logging to capture and display warnings
473
- SwarmSDK::LogCollector.on_log do |log_entry|
474
- @formatter.on_log(log_entry) if log_entry[:type] == "model_lookup_warning"
475
- end
476
-
477
- SwarmSDK::LogStream.emitter = SwarmSDK::LogCollector
478
-
479
- # Emit validation warnings as log events
480
- @swarm.emit_validation_warnings
481
-
482
- # Clean up
483
- SwarmSDK::LogCollector.reset!
484
- SwarmSDK::LogStream.reset!
485
-
486
- # Add spacing if warnings were shown
487
- puts "" if @swarm.validate.any?
488
- rescue StandardError
489
- # Ignore errors during validation emission
490
- begin
491
- SwarmSDK::LogCollector.reset!
492
- rescue
493
- nil
494
- end
495
- begin
496
- SwarmSDK::LogStream.reset!
497
- rescue
498
- nil
499
- end
500
- end
501
-
502
- def display_help
503
- help_box = TTY::Box.frame(
504
- @colors[:header].call("Available Commands:"),
505
- "",
506
- *COMMANDS.map do |cmd, desc|
507
- cmd_styled = @colors[:code].call(cmd.ljust(15))
508
- desc_styled = @colors[:system].call(desc)
509
- " #{cmd_styled} #{desc_styled}"
510
- end,
511
- "",
512
- @colors[:system].call("Input Tips:"),
513
- @colors[:system].call(" • Press Enter to submit your message"),
514
- @colors[:system].call(" • Press Option+Enter (or ESC then Enter) for multi-line input"),
515
- @colors[:system].call(" • Press Ctrl+C to cancel an ongoing request"),
516
- @colors[:system].call(" • Press Ctrl+D to exit"),
517
- @colors[:system].call(" • Use arrow keys for history and editing"),
518
- @colors[:system].call(" • Type / for commands or @ for file paths"),
519
- @colors[:system].call(" • Use Shift-Tab to navigate autocomplete menu"),
520
- border: :light,
521
- padding: [1, 2],
522
- align: :left,
523
- title: { top_left: " HELP " },
524
- style: {
525
- border: { fg: :bright_yellow },
526
- },
527
- )
528
-
529
- puts help_box
530
- end
531
-
532
- def clear_context
533
- # Get the lead agent
534
- lead = @swarm.agent(@swarm.lead_agent)
535
-
536
- # Clear the agent's conversation history
537
- lead.replace_messages([])
538
-
539
- # Clear REPL conversation history
540
- @conversation_history.clear
541
-
542
- # Display confirmation
543
- puts ""
544
- puts @colors[:success].call("✓ Conversation context cleared for #{@swarm.lead_agent}")
545
- puts @colors[:system].call(" Starting fresh - previous messages removed from context")
546
- puts ""
547
- end
548
-
549
- def list_tools
550
- # Get the lead agent
551
- lead = @swarm.agent(@swarm.lead_agent)
552
-
553
- # Get tools hash (tool_name => tool_instance)
554
- tools_hash = lead.tools
555
-
556
- puts ""
557
- puts @colors[:header].call("Available Tools for #{@swarm.lead_agent}:")
558
- puts @colors[:divider].call("─" * 60)
559
- puts ""
560
-
561
- if tools_hash.empty?
562
- puts @colors[:system].call("No tools available")
563
- return
564
- end
565
-
566
- # Group tools by category
567
- memory_tools = []
568
- standard_tools = []
569
- delegation_tools = []
570
- mcp_tools = []
571
- other_tools = []
572
-
573
- tools_hash.each_value do |tool|
574
- tool_name = tool.name
575
- case tool_name
576
- when /^Memory/, "LoadSkill"
577
- memory_tools << tool_name
578
- when /^WorkWith/
579
- delegation_tools << tool_name
580
- when /^mcp__/
581
- mcp_tools << tool_name
582
- when "Read", "Write", "Edit", "MultiEdit", "Bash", "Grep", "Glob",
583
- "TodoWrite", "Think", "Clock", "WebFetch",
584
- "ScratchpadWrite", "ScratchpadRead", "ScratchpadList"
585
- standard_tools << tool_name
586
- else
587
- other_tools << tool_name
588
- end
589
- end
590
-
591
- # Display tools by category
592
- if standard_tools.any?
593
- puts @colors[:agent_label].call("Standard Tools:")
594
- standard_tools.sort.each do |name|
595
- puts @colors[:system].call(" • #{name}")
596
- end
597
- puts ""
598
- end
599
-
600
- if memory_tools.any?
601
- puts @colors[:agent_label].call("Memory Tools:")
602
- memory_tools.sort.each do |name|
603
- puts @colors[:system].call(" • #{name}")
604
- end
605
- puts ""
606
- end
607
-
608
- if delegation_tools.any?
609
- puts @colors[:agent_label].call("Delegation Tools:")
610
- delegation_tools.sort.each do |name|
611
- puts @colors[:system].call(" • #{name}")
612
- end
613
- puts ""
614
- end
615
-
616
- if mcp_tools.any?
617
- puts @colors[:agent_label].call("MCP Tools:")
618
- mcp_tools.sort.each do |name|
619
- puts @colors[:system].call(" • #{name}")
620
- end
621
- puts ""
622
- end
623
-
624
- if other_tools.any?
625
- puts @colors[:agent_label].call("Other Tools:")
626
- other_tools.sort.each do |name|
627
- puts @colors[:system].call(" • #{name}")
628
- end
629
- puts ""
630
- end
631
-
632
- puts @colors[:divider].call("─" * 60)
633
- puts @colors[:system].call("Total: #{tools_hash.size} tools")
634
- puts ""
635
- end
636
-
637
- def display_history
638
- if @conversation_history.empty?
639
- puts @colors[:system].call("No conversation history yet")
640
- return
641
- end
642
-
643
- puts @colors[:header].call("Conversation History:")
644
- puts @colors[:divider].call("─" * 60)
645
- puts ""
646
-
647
- @conversation_history.each_with_index do |entry, index|
648
- role_label = if entry[:role] == "user"
649
- @colors[:prompt].call("User")
650
- else
651
- @colors[:agent_label].call("Agent")
652
- end
653
-
654
- puts "#{index + 1}. #{role_label}:"
655
-
656
- # Truncate long messages in history view
657
- content = entry[:content]
658
- if content.length > 200
659
- content = content[0...200] + "..."
660
- end
661
-
662
- puts @colors[:system].call(" #{content.gsub("\n", "\n ")}")
663
- puts ""
664
- end
665
-
666
- puts @colors[:divider].call("─" * 60)
667
- end
668
-
669
- def defrag_memory
670
- puts ""
671
- puts @colors[:header].call("🔧 Memory Defragmentation Workflow")
672
- puts @colors[:divider].call("─" * 60)
673
- puts ""
674
-
675
- # Inject prompt to run find_related then link_related
676
- prompt = <<~PROMPT.strip
677
- Run memory defragmentation workflow:
678
-
679
- 1. First, run MemoryDefrag(action: "find_related") to discover related entries
680
- 2. Review the results carefully
681
- 3. Then run MemoryDefrag(action: "link_related", dry_run: false) to create bidirectional links
682
-
683
- Report what you found and what links were created.
684
- PROMPT
685
-
686
- handle_message(prompt)
687
- end
688
-
689
- def display_goodbye
690
- puts ""
691
- goodbye_text = @colors[:success].call("👋 Goodbye! Thanks for using Swarm CLI")
692
- puts goodbye_text
693
- puts ""
694
- end
695
-
696
- def display_session_summary
697
- return if @session_results.empty?
698
-
699
- # Calculate session totals
700
- total_tokens = @session_results.sum(&:total_tokens)
701
- total_cost = @session_results.sum(&:total_cost)
702
- total_llm_requests = @session_results.sum(&:llm_requests)
703
- total_tool_calls = @session_results.sum(&:tool_calls_count)
704
- all_agents = @session_results.flat_map(&:agents_involved).uniq
705
-
706
- # Get session duration (time from first to last message)
707
- session_duration = if @session_results.size > 1
708
- @session_results.map(&:duration).sum
709
- else
710
- @session_results.first&.duration || 0
711
- end
712
-
713
- # Render session summary
714
- divider = @colors[:divider].call("─" * 60)
715
- puts divider
716
- puts @colors[:header].call("📊 Session Summary")
717
- puts divider
718
- puts ""
719
-
720
- # Message count
721
- msg_count = @conversation_history.count { |entry| entry[:role] == "user" }
722
- puts " #{@colors[:agent_label].call("Messages sent:")} #{msg_count}"
723
-
724
- # Agents used
725
- if all_agents.any?
726
- agents_list = all_agents.map { |agent| @colors[:agent_label].call(agent.to_s) }.join(", ")
727
- puts " #{@colors[:agent_label].call("Agents used:")} #{agents_list}"
728
- end
729
-
730
- # LLM requests
731
- puts " #{@colors[:system].call("LLM Requests:")} #{total_llm_requests}"
732
-
733
- # Tool calls
734
- puts " #{@colors[:system].call("Tool Calls:")} #{total_tool_calls}"
735
-
736
- # Tokens
737
- formatted_tokens = SwarmCLI::UI::Formatters::Number.format(total_tokens)
738
- puts " #{@colors[:system].call("Total Tokens:")} #{formatted_tokens}"
739
-
740
- # Cost (colored)
741
- formatted_cost = SwarmCLI::UI::Formatters::Cost.format(total_cost, pastel: @pastel)
742
- puts " #{@colors[:system].call("Total Cost:")} #{formatted_cost}"
743
-
744
- # Duration
745
- formatted_duration = SwarmCLI::UI::Formatters::Time.duration(session_duration)
746
- puts " #{@colors[:system].call("Session Duration:")} #{formatted_duration}"
747
-
748
- puts ""
749
- puts divider
750
- puts ""
751
- end
752
-
753
- def render_error(message)
754
- icon = @colors[:error_icon].call("✗")
755
- text = @colors[:error].call(message)
756
- "#{icon} #{text}"
757
- end
758
-
759
- def render_system_message(text)
760
- bracket_open = @colors[:system_bracket].call("[")
761
- bracket_close = @colors[:system_bracket].call("]")
762
- content = @colors[:system].call(text)
763
- "#{bracket_open}#{content}#{bracket_close}"
764
- end
765
-
766
- def configure_completion_ui
767
- # Only configure if Reline::Face is available (Ruby 3.1+)
768
- return unless defined?(Reline::Face)
769
-
770
- Reline::Face.config(:completion_dialog) do |conf|
771
- conf.define(:default, foreground: :white, background: :blue)
772
- conf.define(:enhanced, foreground: :black, background: :cyan) # Selected item
773
- conf.define(:scrollbar, foreground: :cyan, background: :blue)
774
- end
775
- rescue StandardError
776
- # Ignore errors if Face configuration fails
777
- end
778
-
779
- def setup_fuzzy_completion
780
- # Capture COMMANDS for use in lambda
781
- commands = COMMANDS
782
-
783
- # Capture file completion logic for use in lambda (since lambda runs in different context)
784
- file_completions = lambda do |target|
785
- has_at_prefix = target.start_with?("@")
786
- query = has_at_prefix ? target[1..] : target
787
-
788
- next Dir.glob("*").sort.first(20) if query.empty?
789
-
790
- # Find files matching query anywhere in path
791
- pattern = "**/*#{query}*"
792
- found = Dir.glob(pattern, File::FNM_CASEFOLD).reject do |path|
793
- path.split("/").any? { |part| part.start_with?(".") }
794
- end.sort.first(20)
795
-
796
- # Add @ prefix if needed
797
- has_at_prefix ? found.map { |p| "@#{p}" } : found
798
- end
799
-
800
- # Custom dialog proc for fuzzy file/command completion
801
- fuzzy_proc = lambda do
802
- # State: [pre, target, post, matches, pointer, navigating]
803
-
804
- # Check if this is a navigation key press
805
- is_nav_key = key&.match?(dialog.name)
806
-
807
- # If we were in navigation mode and user typed a regular key (not Tab), exit nav mode
808
- if !context.empty? && context.size >= 6 && context[5] && !is_nav_key
809
- context[5] = false # Exit navigation mode
810
- end
811
-
812
- # Early check: if user typed and current target has spaces, close dialog
813
- unless is_nav_key || context.empty?
814
- _, target_check, = retrieve_completion_block
815
- if target_check.include?(" ")
816
- context.clear
817
- return
818
- end
819
- end
820
-
821
- # Detect if we should recalculate matches
822
- should_recalculate = if context.empty?
823
- true # First time - initialize
824
- elsif is_nav_key
825
- false # Navigation key - don't recalculate, just cycle
826
- elsif context.size >= 6 && context[5]
827
- false # We're in navigation mode - keep matches stable
828
- else
829
- true # User typed something - recalculate
830
- end
831
-
832
- # Recalculate matches if user typed
833
- if should_recalculate
834
- preposing, target, postposing = retrieve_completion_block
835
-
836
- # Don't show completions if the target itself has spaces
837
- # (allows "@lib/swarm" in middle of sentence like "check @lib/swarm file")
838
- return if target.include?(" ")
839
-
840
- matches = if target.start_with?("/")
841
- # Command completions
842
- query = target[1..] || ""
843
- commands.keys.map(&:to_s).select do |cmd|
844
- query.empty? || cmd.downcase.include?(query.downcase)
845
- end.sort
846
- elsif target.start_with?("@") || target.include?("/")
847
- # File path completions - use captured lambda
848
- file_completions.call(target)
849
- end
850
-
851
- return if matches.nil? || matches.empty?
852
-
853
- # Store fresh values - not in navigation mode yet
854
- context.clear
855
- context.push(preposing, target, postposing, matches, 0, false)
856
- end
857
-
858
- # Use stored values
859
- stored_pre, _, stored_post, matches, pointer, _ = context
860
-
861
- # Handle navigation keys
862
- if is_nav_key
863
- # Check if Enter was pressed - close dialog without submitting
864
- # Must check key.char (not method_symbol, which is :fuzzy_complete when trapped)
865
- if key.char == "\r" || key.char == "\n"
866
- # Enter pressed - accept completion and close dialog
867
- # Clear context so dialog doesn't reappear
868
- context.clear
869
- return
870
- end
871
-
872
- # Update pointer (cycle through matches)
873
- # Tab is now bound to :fuzzy_complete, Shift-Tab to :completion_journey_up
874
- pointer = if key.method_symbol == :completion_journey_up
875
- # Shift-Tab - cycle backward
876
- (pointer - 1) % matches.size
877
- else
878
- # Tab (:fuzzy_complete) - cycle forward
879
- (pointer + 1) % matches.size
880
- end
881
-
882
- # Update line buffer with selected completion
883
- selected = matches[pointer]
884
-
885
- # Get current line editor state
886
- le = @line_editor
887
-
888
- new_line = stored_pre + selected + stored_post
889
- new_cursor = stored_pre.length + selected.bytesize
890
-
891
- # Update buffer using public APIs
892
- le.set_current_line(new_line)
893
- le.byte_pointer = new_cursor
894
-
895
- # Update state - mark as navigating so we don't recalculate
896
- context[4] = pointer
897
- context[5] = true # Now in navigation mode
898
- end
899
-
900
- # Set visual highlight
901
- dialog.pointer = pointer
902
-
903
- # Trap Shift-Tab and Enter (Tab is already bound to our dialog)
904
- dialog.trap_key = [[27, 91, 90], [13]]
905
-
906
- # Position dropdown
907
- x = [cursor_pos.x, 0].max
908
- y = 0
909
-
910
- # Return dialog
911
- Reline::DialogRenderInfo.new(
912
- pos: Reline::CursorPos.new(x, y),
913
- contents: matches,
914
- scrollbar: true,
915
- height: [15, matches.size].min,
916
- face: :completion_dialog,
917
- )
918
- end
919
-
920
- # Register the custom fuzzy dialog
921
- Reline.add_dialog_proc(:fuzzy_complete, fuzzy_proc, [])
922
- end
923
- end
924
- end