claude_swarm 1.0.0 → 1.0.2

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 (267) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/release.md +1 -1
  3. data/.claude/hooks/lint-code-files.rb +65 -0
  4. data/.rubocop.yml +22 -2
  5. data/CHANGELOG.md +21 -1
  6. data/CLAUDE.md +1 -1
  7. data/CONTRIBUTING.md +69 -0
  8. data/README.md +27 -2
  9. data/Rakefile +71 -3
  10. data/analyze_coverage.rb +94 -0
  11. data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
  12. data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
  13. data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
  14. data/docs/v2/README.md +308 -0
  15. data/docs/v2/guides/claude-code-agents.md +262 -0
  16. data/docs/v2/guides/complete-tutorial.md +3088 -0
  17. data/docs/v2/guides/getting-started.md +1456 -0
  18. data/docs/v2/guides/memory-adapters.md +998 -0
  19. data/docs/v2/guides/plugins.md +816 -0
  20. data/docs/v2/guides/quick-start-cli.md +1745 -0
  21. data/docs/v2/guides/rails-integration.md +1902 -0
  22. data/docs/v2/guides/swarm-memory.md +599 -0
  23. data/docs/v2/reference/cli.md +729 -0
  24. data/docs/v2/reference/ruby-dsl.md +2154 -0
  25. data/docs/v2/reference/yaml.md +1835 -0
  26. data/docs-team-swarm.yml +2222 -0
  27. data/examples/learning-assistant/assistant.md +7 -0
  28. data/examples/learning-assistant/example-memories/concept-example.md +90 -0
  29. data/examples/learning-assistant/example-memories/experience-example.md +66 -0
  30. data/examples/learning-assistant/example-memories/fact-example.md +76 -0
  31. data/examples/learning-assistant/example-memories/memory-index.md +78 -0
  32. data/examples/learning-assistant/example-memories/skill-example.md +168 -0
  33. data/examples/learning-assistant/learning_assistant.rb +34 -0
  34. data/examples/learning-assistant/learning_assistant.yml +20 -0
  35. data/examples/v2/dsl/01_basic.rb +44 -0
  36. data/examples/v2/dsl/02_core_parameters.rb +59 -0
  37. data/examples/v2/dsl/03_capabilities.rb +71 -0
  38. data/examples/v2/dsl/04_llm_parameters.rb +56 -0
  39. data/examples/v2/dsl/05_advanced_flags.rb +73 -0
  40. data/examples/v2/dsl/06_permissions.rb +80 -0
  41. data/examples/v2/dsl/07_mcp_server.rb +62 -0
  42. data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
  43. data/examples/v2/dsl/09_agent_hooks.rb +67 -0
  44. data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
  45. data/examples/v2/dsl/11_delegation.rb +60 -0
  46. data/examples/v2/dsl/12_complete_integration.rb +137 -0
  47. data/examples/v2/file_tools_swarm.yml +102 -0
  48. data/examples/v2/hooks/01_basic_hooks.rb +133 -0
  49. data/examples/v2/hooks/02_usage_tracking.rb +201 -0
  50. data/examples/v2/hooks/03_production_monitoring.rb +429 -0
  51. data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
  52. data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
  53. data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
  54. data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
  55. data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
  56. data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
  57. data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
  58. data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
  59. data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
  60. data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
  61. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
  62. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
  63. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
  64. data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
  65. data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
  66. data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
  67. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
  68. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
  69. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
  70. data/examples/v2/hooks/swarm_summary.sh +44 -0
  71. data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
  72. data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
  73. data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
  74. data/examples/v2/hooks/validate_bash.rb +59 -0
  75. data/examples/v2/multi_directory_permissions.yml +221 -0
  76. data/examples/v2/node_context_demo.rb +127 -0
  77. data/examples/v2/node_workflow.rb +173 -0
  78. data/examples/v2/path_resolution_demo.rb +216 -0
  79. data/examples/v2/simple-swarm-v2.rb +90 -0
  80. data/examples/v2/simple-swarm-v2.yml +62 -0
  81. data/examples/v2/swarm.yml +71 -0
  82. data/examples/v2/swarm_with_hooks.yml +61 -0
  83. data/examples/v2/swarm_with_hooks_simple.yml +25 -0
  84. data/examples/v2/think_tool_demo.rb +62 -0
  85. data/exe/swarm +6 -0
  86. data/lib/claude_swarm/claude_mcp_server.rb +0 -6
  87. data/lib/claude_swarm/cli.rb +10 -3
  88. data/lib/claude_swarm/commands/ps.rb +19 -20
  89. data/lib/claude_swarm/commands/show.rb +1 -1
  90. data/lib/claude_swarm/configuration.rb +10 -12
  91. data/lib/claude_swarm/mcp_generator.rb +10 -1
  92. data/lib/claude_swarm/orchestrator.rb +73 -49
  93. data/lib/claude_swarm/system_utils.rb +37 -11
  94. data/lib/claude_swarm/version.rb +1 -1
  95. data/lib/claude_swarm/worktree_manager.rb +1 -0
  96. data/lib/claude_swarm/yaml_loader.rb +22 -0
  97. data/lib/claude_swarm.rb +7 -3
  98. data/lib/swarm_cli/cli.rb +201 -0
  99. data/lib/swarm_cli/command_registry.rb +61 -0
  100. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  101. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  102. data/lib/swarm_cli/commands/migrate.rb +55 -0
  103. data/lib/swarm_cli/commands/run.rb +173 -0
  104. data/lib/swarm_cli/config_loader.rb +97 -0
  105. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  106. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  107. data/lib/swarm_cli/interactive_repl.rb +918 -0
  108. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  109. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  110. data/lib/swarm_cli/migrate_options.rb +54 -0
  111. data/lib/swarm_cli/migrator.rb +132 -0
  112. data/lib/swarm_cli/options.rb +151 -0
  113. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  114. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  115. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  116. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  117. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  118. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  119. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  120. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  121. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  122. data/lib/swarm_cli/ui/icons.rb +59 -0
  123. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  124. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  125. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  126. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  127. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  128. data/lib/swarm_cli/version.rb +5 -0
  129. data/lib/swarm_cli.rb +44 -0
  130. data/lib/swarm_memory/adapters/base.rb +141 -0
  131. data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
  132. data/lib/swarm_memory/chat_extension.rb +34 -0
  133. data/lib/swarm_memory/cli/commands.rb +306 -0
  134. data/lib/swarm_memory/core/entry.rb +37 -0
  135. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  136. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  137. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  138. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  139. data/lib/swarm_memory/core/storage.rb +288 -0
  140. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  141. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  142. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  143. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  144. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  145. data/lib/swarm_memory/errors.rb +21 -0
  146. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  147. data/lib/swarm_memory/integration/configuration.rb +43 -0
  148. data/lib/swarm_memory/integration/registration.rb +31 -0
  149. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  150. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  151. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  152. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  153. data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
  154. data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
  155. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
  156. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  157. data/lib/swarm_memory/search/text_search.rb +42 -0
  158. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  159. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  160. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  161. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  162. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  163. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  164. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  165. data/lib/swarm_memory/tools/memory_glob.rb +160 -0
  166. data/lib/swarm_memory/tools/memory_grep.rb +247 -0
  167. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  168. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  169. data/lib/swarm_memory/tools/memory_write.rb +231 -0
  170. data/lib/swarm_memory/utils.rb +50 -0
  171. data/lib/swarm_memory/version.rb +5 -0
  172. data/lib/swarm_memory.rb +166 -0
  173. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  174. data/lib/swarm_sdk/agent/builder.rb +461 -0
  175. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  176. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  177. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  178. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  179. data/lib/swarm_sdk/agent/chat.rb +1159 -0
  180. data/lib/swarm_sdk/agent/context.rb +112 -0
  181. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  182. data/lib/swarm_sdk/agent/definition.rb +556 -0
  183. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  184. data/lib/swarm_sdk/configuration.rb +296 -0
  185. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  186. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  187. data/lib/swarm_sdk/context_compactor.rb +340 -0
  188. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  189. data/lib/swarm_sdk/hooks/context.rb +197 -0
  190. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  191. data/lib/swarm_sdk/hooks/error.rb +29 -0
  192. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  193. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  194. data/lib/swarm_sdk/hooks/result.rb +150 -0
  195. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  196. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  197. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  198. data/lib/swarm_sdk/log_collector.rb +51 -0
  199. data/lib/swarm_sdk/log_stream.rb +69 -0
  200. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  201. data/lib/swarm_sdk/model_aliases.json +5 -0
  202. data/lib/swarm_sdk/models.json +1 -0
  203. data/lib/swarm_sdk/models.rb +120 -0
  204. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  205. data/lib/swarm_sdk/node/builder.rb +439 -0
  206. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  207. data/lib/swarm_sdk/node_context.rb +170 -0
  208. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  209. data/lib/swarm_sdk/permissions/config.rb +239 -0
  210. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  211. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  212. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  213. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  214. data/lib/swarm_sdk/plugin.rb +147 -0
  215. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  216. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  217. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  218. data/lib/swarm_sdk/result.rb +97 -0
  219. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  220. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  221. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  222. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  223. data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
  224. data/lib/swarm_sdk/swarm.rb +982 -0
  225. data/lib/swarm_sdk/tools/bash.rb +274 -0
  226. data/lib/swarm_sdk/tools/clock.rb +44 -0
  227. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  228. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  229. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  230. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  231. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  232. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  233. data/lib/swarm_sdk/tools/edit.rb +150 -0
  234. data/lib/swarm_sdk/tools/glob.rb +158 -0
  235. data/lib/swarm_sdk/tools/grep.rb +228 -0
  236. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  237. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  238. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  239. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  240. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  241. data/lib/swarm_sdk/tools/read.rb +251 -0
  242. data/lib/swarm_sdk/tools/registry.rb +93 -0
  243. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  244. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  245. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  246. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  247. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  248. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  249. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  250. data/lib/swarm_sdk/tools/think.rb +95 -0
  251. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  252. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  253. data/lib/swarm_sdk/tools/write.rb +117 -0
  254. data/lib/swarm_sdk/utils.rb +50 -0
  255. data/lib/swarm_sdk/version.rb +5 -0
  256. data/lib/swarm_sdk.rb +157 -0
  257. data/llm.v2.txt +13407 -0
  258. data/rubocop/cop/security/no_reflection_methods.rb +47 -0
  259. data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
  260. data/swarm_cli.gemspec +57 -0
  261. data/swarm_memory.gemspec +28 -0
  262. data/swarm_sdk.gemspec +41 -0
  263. data/team.yml +1 -1
  264. data/team_full.yml +1875 -0
  265. data/{team_v2.yml → team_sdk.yml} +121 -52
  266. metadata +247 -4
  267. data/EXAMPLES.md +0 -164
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module State
6
+ # Tracks agent depth for hierarchical indentation display
7
+ class DepthTracker
8
+ def initialize
9
+ @depths = {}
10
+ @seen_agents = []
11
+ end
12
+
13
+ # Get indentation depth for agent
14
+ def get(agent_name)
15
+ @depths[agent_name] ||= calculate_depth(agent_name)
16
+ end
17
+
18
+ # Get indent string for agent
19
+ def indent(agent_name, char: " ")
20
+ char * get(agent_name)
21
+ end
22
+
23
+ # Reset tracker (for testing)
24
+ def reset
25
+ @depths.clear
26
+ @seen_agents.clear
27
+ end
28
+
29
+ private
30
+
31
+ def calculate_depth(agent_name)
32
+ @seen_agents << agent_name unless @seen_agents.include?(agent_name)
33
+
34
+ # First agent is depth 0, all others are depth 1
35
+ @seen_agents.size == 1 ? 0 : 1
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module State
6
+ # Manages active spinners with elapsed time display
7
+ class SpinnerManager
8
+ def initialize
9
+ @active_spinners = {}
10
+ @time_updaters = {}
11
+ end
12
+
13
+ # Start a spinner with elapsed time tracking
14
+ #
15
+ # @param key [Symbol, String] Unique key for this spinner
16
+ # @param message [String] Spinner message
17
+ # @param format [Symbol] Spinner format (:dots, :pulse, etc.)
18
+ # @return [TTY::Spinner] The spinner instance
19
+ def start(key, message, format: :dots)
20
+ # Stop any existing spinner with this key
21
+ stop(key) if @active_spinners[key]
22
+
23
+ # Create spinner with elapsed time token
24
+ spinner = TTY::Spinner.new(
25
+ "[:spinner] #{message} (:elapsed)",
26
+ format: format,
27
+ hide_cursor: true,
28
+ )
29
+
30
+ spinner.auto_spin
31
+
32
+ # Spawn thread to update elapsed time every 1 second
33
+ # This is 10x slower than spinner animation (100ms), preventing flicker
34
+ @time_updaters[key] = Thread.new do
35
+ loop do
36
+ elapsed = spinner.duration
37
+ break unless elapsed
38
+
39
+ formatted_time = format_duration(elapsed)
40
+ spinner.update(elapsed: formatted_time)
41
+ sleep(1.0) # 1s refresh rate - smooth without flicker
42
+ rescue StandardError
43
+ break
44
+ end
45
+ end
46
+
47
+ @active_spinners[key] = spinner
48
+ spinner
49
+ end
50
+
51
+ # Stop spinner with success
52
+ #
53
+ # @param key [Symbol, String] Spinner key
54
+ # @param message [String] Success message
55
+ def success(key, message = "completed")
56
+ spinner = @active_spinners[key]
57
+ return unless spinner
58
+
59
+ # Kill time updater
60
+ kill_updater(key)
61
+
62
+ # Show final time
63
+ final_time = format_duration(spinner.duration || 0)
64
+ spinner.success("#{message} (#{final_time})")
65
+
66
+ cleanup(key)
67
+ end
68
+
69
+ # Stop spinner with error
70
+ #
71
+ # @param key [Symbol, String] Spinner key
72
+ # @param message [String] Error message
73
+ def error(key, message = "failed")
74
+ spinner = @active_spinners[key]
75
+ return unless spinner
76
+
77
+ # Kill time updater
78
+ kill_updater(key)
79
+
80
+ # Show final time
81
+ final_time = format_duration(spinner.duration || 0)
82
+ spinner.error("#{message} (#{final_time})")
83
+
84
+ cleanup(key)
85
+ end
86
+
87
+ # Stop spinner without success/error (just stop)
88
+ #
89
+ # @param key [Symbol, String] Spinner key
90
+ def stop(key)
91
+ spinner = @active_spinners[key]
92
+ return unless spinner
93
+
94
+ kill_updater(key)
95
+ spinner.stop
96
+ cleanup(key)
97
+ end
98
+
99
+ # Stop all active spinners
100
+ def stop_all
101
+ @active_spinners.keys.each { |key| stop(key) }
102
+ end
103
+
104
+ # Check if a spinner is active
105
+ #
106
+ # @param key [Symbol, String] Spinner key
107
+ # @return [Boolean]
108
+ def active?(key)
109
+ @active_spinners.key?(key)
110
+ end
111
+
112
+ # Pause all active spinners (for interactive debugging)
113
+ #
114
+ # This temporarily stops spinner animation while preserving state,
115
+ # allowing interactive sessions like binding.irb to run cleanly.
116
+ #
117
+ # @return [void]
118
+ def pause_all
119
+ @active_spinners.each_value do |spinner|
120
+ spinner.stop if spinner.spinning?
121
+ end
122
+
123
+ # Keep time updaters running (they'll safely handle stopped spinners)
124
+ end
125
+
126
+ # Resume all paused spinners
127
+ #
128
+ # Restarts spinner animation for all spinners that were paused.
129
+ #
130
+ # @return [void]
131
+ def resume_all
132
+ @active_spinners.each_value do |spinner|
133
+ spinner.auto_spin unless spinner.spinning?
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ def kill_updater(key)
140
+ updater = @time_updaters[key]
141
+ return unless updater
142
+
143
+ updater.kill if updater.alive?
144
+ @time_updaters.delete(key)
145
+ end
146
+
147
+ def cleanup(key)
148
+ @active_spinners.delete(key)
149
+ @time_updaters.delete(key)
150
+ end
151
+
152
+ def format_duration(seconds)
153
+ if seconds < 1
154
+ "#{(seconds * 1000).round}ms"
155
+ elsif seconds < 60
156
+ "#{seconds.round}s"
157
+ elsif seconds < 3600
158
+ minutes = (seconds / 60).floor
159
+ secs = (seconds % 60).round
160
+ "#{minutes}m #{secs}s"
161
+ else
162
+ hours = (seconds / 3600).floor
163
+ minutes = ((seconds % 3600) / 60).floor
164
+ "#{hours}h #{minutes}m"
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module State
6
+ # Tracks cumulative usage statistics during swarm execution
7
+ class UsageTracker
8
+ attr_reader :total_cost, :total_tokens, :llm_requests, :tool_calls
9
+
10
+ def initialize
11
+ @total_cost = 0.0
12
+ @total_tokens = 0
13
+ @llm_requests = 0
14
+ @tool_calls = 0
15
+ @agents_seen = Set.new
16
+ @recent_tool_calls = {} # tool_call_id => tool_name for matching
17
+ end
18
+
19
+ # Track an LLM API call
20
+ def track_llm_request(usage_data)
21
+ @llm_requests += 1
22
+
23
+ if usage_data
24
+ @total_cost += usage_data[:total_cost] || 0.0
25
+ @total_tokens += usage_data[:total_tokens] || 0
26
+ end
27
+ end
28
+
29
+ # Track a tool call
30
+ def track_tool_call(tool_call_id: nil, tool_name: nil)
31
+ @tool_calls += 1
32
+ @recent_tool_calls[tool_call_id] = tool_name if tool_call_id && tool_name
33
+ end
34
+
35
+ # Track agent usage
36
+ def track_agent(agent_name)
37
+ @agents_seen.add(agent_name)
38
+ end
39
+
40
+ # Get list of agents seen
41
+ def agents
42
+ @agents_seen.to_a
43
+ end
44
+
45
+ # Get tool name from call ID
46
+ def tool_name_for(tool_call_id)
47
+ @recent_tool_calls[tool_call_id]
48
+ end
49
+
50
+ # Reset all counters (for testing)
51
+ def reset
52
+ @total_cost = 0.0
53
+ @total_tokens = 0
54
+ @llm_requests = 0
55
+ @tool_calls = 0
56
+ @agents_seen.clear
57
+ @recent_tool_calls.clear
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ VERSION = "2.1.0"
5
+ end
data/lib/swarm_cli.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "pathname"
6
+ require "yaml"
7
+
8
+ require "reline"
9
+ require "pastel"
10
+ require "tty-box"
11
+ require "tty-screen"
12
+ require "tty/link"
13
+ require "tty/markdown"
14
+ require "tty/option"
15
+ require "tty/spinner"
16
+ require "tty/spinner/multi"
17
+ require "tty/tree"
18
+
19
+ require "swarm_sdk"
20
+
21
+ require_relative "swarm_cli/version"
22
+
23
+ require "zeitwerk"
24
+ loader = Zeitwerk::Loader.new
25
+ loader.push_dir("#{__dir__}/swarm_cli", namespace: SwarmCLI)
26
+ loader.inflector.inflect(
27
+ "cli" => "CLI",
28
+ "ui" => "UI",
29
+ "interactive_repl" => "InteractiveREPL",
30
+ )
31
+ loader.setup
32
+
33
+ module SwarmCLI
34
+ class Error < StandardError; end
35
+ class ConfigurationError < Error; end
36
+ class ExecutionError < Error; end
37
+ end
38
+
39
+ # Try to load swarm_memory gem if available (for CLI command extensions)
40
+ begin
41
+ require "swarm_memory"
42
+ rescue LoadError
43
+ # swarm_memory not installed - that's fine, memory commands won't be available
44
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmMemory
4
+ module Adapters
5
+ # Abstract base adapter interface for memory storage backends
6
+ #
7
+ # Subclasses must implement all public methods to provide
8
+ # different storage backends (filesystem, Redis, SQLite, etc.)
9
+ class Base
10
+ # Maximum size per entry (1MB)
11
+ MAX_ENTRY_SIZE = 1_000_000
12
+
13
+ # Maximum total storage size (100MB)
14
+ MAX_TOTAL_SIZE = 100_000_000
15
+
16
+ # Write content to storage
17
+ #
18
+ # @param file_path [String] Path to store content
19
+ # @param content [String] Content to store
20
+ # @param title [String] Brief title describing the content
21
+ # @param embedding [Array<Float>, nil] Optional embedding vector
22
+ # @param metadata [Hash, nil] Optional metadata
23
+ # @raise [ArgumentError] If size limits are exceeded
24
+ # @return [Core::Entry] The created entry
25
+ def write(file_path:, content:, title:, embedding: nil, metadata: nil)
26
+ raise NotImplementedError, "Subclass must implement #write"
27
+ end
28
+
29
+ # Read content from storage
30
+ #
31
+ # @param file_path [String] Path to read from
32
+ # @raise [ArgumentError] If path not found
33
+ # @return [String] Content at the path
34
+ def read(file_path:)
35
+ raise NotImplementedError, "Subclass must implement #read"
36
+ end
37
+
38
+ # Read full entry with metadata
39
+ #
40
+ # @param file_path [String] Path to read from
41
+ # @raise [ArgumentError] If path not found
42
+ # @return [Core::Entry] Full entry object
43
+ def read_entry(file_path:)
44
+ raise NotImplementedError, "Subclass must implement #read_entry"
45
+ end
46
+
47
+ # Delete a specific entry
48
+ #
49
+ # @param file_path [String] Path to delete
50
+ # @raise [ArgumentError] If path not found
51
+ # @return [void]
52
+ def delete(file_path:)
53
+ raise NotImplementedError, "Subclass must implement #delete"
54
+ end
55
+
56
+ # List entries, optionally filtered by prefix
57
+ #
58
+ # @param prefix [String, nil] Filter by path prefix
59
+ # @return [Array<Hash>] Array of entry metadata (path, title, size, updated_at)
60
+ def list(prefix: nil)
61
+ raise NotImplementedError, "Subclass must implement #list"
62
+ end
63
+
64
+ # Search entries by glob pattern
65
+ #
66
+ # @param pattern [String] Glob pattern (e.g., "**/*.txt", "parallel/*/task_*")
67
+ # @return [Array<Hash>] Array of matching entry metadata, sorted by most recent first
68
+ def glob(pattern:)
69
+ raise NotImplementedError, "Subclass must implement #glob"
70
+ end
71
+
72
+ # Search entry content by pattern
73
+ #
74
+ # @param pattern [String] Regular expression pattern to search for
75
+ # @param case_insensitive [Boolean] Whether to perform case-insensitive search
76
+ # @param output_mode [String] Output mode: "files_with_matches" (default), "content", or "count"
77
+ # @param path [String, nil] Optional path prefix filter (e.g., "concept/", "fact/api-design")
78
+ # @return [Array<Hash>, String] Results based on output_mode
79
+ def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches", path: nil)
80
+ raise NotImplementedError, "Subclass must implement #grep"
81
+ end
82
+
83
+ # Clear all entries
84
+ #
85
+ # @return [void]
86
+ def clear
87
+ raise NotImplementedError, "Subclass must implement #clear"
88
+ end
89
+
90
+ # Get current total size
91
+ #
92
+ # @return [Integer] Total size in bytes
93
+ def total_size
94
+ raise NotImplementedError, "Subclass must implement #total_size"
95
+ end
96
+
97
+ # Get number of entries
98
+ #
99
+ # @return [Integer] Number of entries
100
+ def size
101
+ raise NotImplementedError, "Subclass must implement #size"
102
+ end
103
+
104
+ protected
105
+
106
+ # Format bytes to human-readable size
107
+ #
108
+ # @param bytes [Integer] Number of bytes
109
+ # @return [String] Formatted size (e.g., "1.5MB", "500.0KB")
110
+ def format_bytes(bytes)
111
+ if bytes >= 1_000_000
112
+ "#{(bytes.to_f / 1_000_000).round(1)}MB"
113
+ elsif bytes >= 1_000
114
+ "#{(bytes.to_f / 1_000).round(1)}KB"
115
+ else
116
+ "#{bytes}B"
117
+ end
118
+ end
119
+
120
+ # Convert glob pattern to regex
121
+ #
122
+ # @param pattern [String] Glob pattern
123
+ # @return [Regexp] Regular expression
124
+ def glob_to_regex(pattern)
125
+ # Escape special regex characters except glob wildcards
126
+ escaped = Regexp.escape(pattern)
127
+
128
+ # Convert glob wildcards to regex
129
+ # ** matches any number of directories (including zero)
130
+ escaped = escaped.gsub('\*\*', ".*")
131
+ # * matches anything except directory separator
132
+ escaped = escaped.gsub('\*', "[^/]*")
133
+ # ? matches single character except directory separator
134
+ escaped = escaped.gsub('\?', "[^/]")
135
+
136
+ # Anchor to start and end
137
+ Regexp.new("\\A#{escaped}\\z")
138
+ end
139
+ end
140
+ end
141
+ end