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,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module Components
6
+ # Renders highlighted panels for warnings, info, errors
7
+ # Uses top/bottom borders only (no sides per design constraint)
8
+ class Panel
9
+ TYPE_CONFIGS = {
10
+ warning: { color: :yellow, icon: UI::Icons::WARNING },
11
+ error: { color: :red, icon: UI::Icons::ERROR },
12
+ info: { color: :cyan, icon: UI::Icons::INFO },
13
+ success: { color: :green, icon: UI::Icons::SUCCESS },
14
+ }.freeze
15
+
16
+ def initialize(pastel:, terminal_width: 80)
17
+ @pastel = pastel
18
+ @terminal_width = terminal_width
19
+ end
20
+
21
+ # Render panel with top/bottom borders
22
+ #
23
+ # ⚠️ CONTEXT WARNING
24
+ # Context usage: 81.4% (threshold: 80%)
25
+ # Tokens remaining: 74,523
26
+ #
27
+ def render(type:, title:, lines:, indent: 0)
28
+ config = TYPE_CONFIGS[type] || TYPE_CONFIGS[:info]
29
+ prefix = " " * indent
30
+
31
+ output = []
32
+
33
+ # Title line with icon
34
+ icon = config[:icon]
35
+ colored_title = @pastel.public_send(config[:color], "#{icon} #{title}")
36
+ output << "#{prefix}#{colored_title}"
37
+
38
+ # Content lines
39
+ lines.each do |line|
40
+ output << "#{prefix} #{line}"
41
+ end
42
+
43
+ output << "" # Blank line after panel
44
+
45
+ output.join("\n")
46
+ end
47
+
48
+ # Render compact panel (single line)
49
+ # ⚠️ Context approaching limit (81.4%)
50
+ def render_compact(type:, message:, indent: 0)
51
+ config = TYPE_CONFIGS[type] || TYPE_CONFIGS[:info]
52
+ prefix = " " * indent
53
+
54
+ icon = config[:icon]
55
+ colored_msg = @pastel.public_send(config[:color], "#{icon} #{message}")
56
+
57
+ "#{prefix}#{colored_msg}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module Components
6
+ # Renders usage statistics (tokens, cost, context percentage)
7
+ class UsageStats
8
+ def initialize(pastel:)
9
+ @pastel = pastel
10
+ end
11
+
12
+ # Render usage line with all available metrics
13
+ # 5,922 tokens │ $0.0016 │ 1.5% used, 394,078 remaining
14
+ def render(tokens:, cost:, context_pct: nil, remaining: nil, cumulative: nil)
15
+ parts = []
16
+
17
+ # Token count (always shown)
18
+ parts << "#{Formatters::Number.format(tokens)} tokens"
19
+
20
+ # Cost (always shown if > 0)
21
+ parts << Formatters::Cost.format(cost, pastel: @pastel) if cost > 0
22
+
23
+ # Context tracking (if available)
24
+ if context_pct
25
+ colored_pct = color_context_percentage(context_pct)
26
+
27
+ parts << if remaining
28
+ "#{colored_pct} used, #{Formatters::Number.compact(remaining)} remaining"
29
+ else
30
+ "#{colored_pct} used"
31
+ end
32
+ elsif cumulative
33
+ # Model doesn't have context limit, show cumulative
34
+ parts << "#{Formatters::Number.compact(cumulative)} cumulative"
35
+ end
36
+
37
+ @pastel.dim(parts.join(" #{@pastel.dim("│")} "))
38
+ end
39
+
40
+ # Render compact stats for prompt display
41
+ # 15.2K tokens • $0.045 • 3.8% context
42
+ def render_compact(tokens:, cost:, context_pct: nil)
43
+ parts = []
44
+
45
+ parts << "#{Formatters::Number.compact(tokens)} tokens" if tokens > 0
46
+ parts << Formatters::Cost.format_plain(cost) if cost > 0
47
+ parts << "#{context_pct} context" if context_pct
48
+
49
+ parts.join(" • ")
50
+ end
51
+
52
+ private
53
+
54
+ def color_context_percentage(percentage_string)
55
+ percentage = percentage_string.to_s.gsub("%", "").to_f
56
+
57
+ color = if percentage < 50
58
+ :green
59
+ elsif percentage < 80
60
+ :yellow
61
+ else
62
+ :red
63
+ end
64
+
65
+ @pastel.public_send(color, percentage_string)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module Formatters
6
+ # Cost formatting with color coding
7
+ class Cost
8
+ class << self
9
+ # Format cost with appropriate precision and color
10
+ # Small costs: green, $0.001234
11
+ # Medium costs: yellow, $0.1234
12
+ # Large costs: red, $12.34
13
+ def format(cost, pastel:)
14
+ return pastel.dim("$0.0000") if cost.nil? || cost.zero?
15
+
16
+ formatted = if cost < 0.01
17
+ Kernel.format("%.6f", cost)
18
+ elsif cost < 1.0
19
+ Kernel.format("%.4f", cost)
20
+ else
21
+ Kernel.format("%.2f", cost)
22
+ end
23
+
24
+ if cost < 0.01
25
+ pastel.green("$#{formatted}")
26
+ elsif cost < 1.0
27
+ pastel.yellow("$#{formatted}")
28
+ else
29
+ pastel.red("$#{formatted}")
30
+ end
31
+ end
32
+
33
+ # Format cost without color (for plain text)
34
+ def format_plain(cost)
35
+ return "$0.0000" if cost.nil? || cost.zero?
36
+
37
+ if cost < 0.01
38
+ Kernel.format("$%.6f", cost)
39
+ elsif cost < 1.0
40
+ Kernel.format("$%.4f", cost)
41
+ else
42
+ Kernel.format("$%.2f", cost)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module Formatters
6
+ # Number formatting utilities for terminal display
7
+ class Number
8
+ class << self
9
+ # Format number with thousand separators
10
+ # 5922 → "5,922"
11
+ # 1500000 → "1,500,000"
12
+ def format(num)
13
+ return "0" if num.nil? || num.zero?
14
+
15
+ num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
16
+ end
17
+
18
+ # Format number with compact units (K, M, B)
19
+ # 5922 → "5.9K"
20
+ # 1500000 → "1.5M"
21
+ # 1500000000 → "1.5B"
22
+ def compact(num)
23
+ return "0" if num.nil? || num.zero?
24
+
25
+ case num
26
+ when 0...1_000
27
+ num.to_s
28
+ when 1_000...1_000_000
29
+ "#{(num / 1_000.0).round(1)}K"
30
+ when 1_000_000...1_000_000_000
31
+ "#{(num / 1_000_000.0).round(1)}M"
32
+ else
33
+ "#{(num / 1_000_000_000.0).round(1)}B"
34
+ end
35
+ end
36
+
37
+ # Format bytes with units (KB, MB, GB)
38
+ # 1024 → "1.0 KB"
39
+ # 1500000 → "1.4 MB"
40
+ def bytes(num)
41
+ return "0 B" if num.nil? || num.zero?
42
+
43
+ case num
44
+ when 0...1024
45
+ "#{num} B"
46
+ when 1024...1024**2
47
+ "#{(num / 1024.0).round(1)} KB"
48
+ when 1024**2...1024**3
49
+ "#{(num / 1024.0**2).round(1)} MB"
50
+ else
51
+ "#{(num / 1024.0**3).round(1)} GB"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module Formatters
6
+ # Text manipulation utilities for clean display
7
+ class Text
8
+ class << self
9
+ # Strip <system-reminder> tags from content
10
+ def strip_system_reminders(text)
11
+ return "" if text.nil?
12
+
13
+ text.gsub(%r{<system-reminder>.*?</system-reminder>}m, "").strip
14
+ end
15
+
16
+ # Truncate text to specified character/line limits
17
+ # Returns [display_text, truncation_message]
18
+ def truncate(text, chars: nil, lines: nil)
19
+ return [text, nil] if text.nil? || text.empty?
20
+
21
+ text_lines = text.split("\n")
22
+ truncated = false
23
+ truncation_parts = []
24
+
25
+ # Apply line limit
26
+ if lines && text_lines.length > lines
27
+ text_lines = text_lines.first(lines)
28
+ hidden_lines = text.split("\n").length - lines
29
+ truncation_parts << "#{hidden_lines} more lines"
30
+ truncated = true
31
+ end
32
+
33
+ result_text = text_lines.join("\n")
34
+
35
+ # Apply character limit
36
+ if chars && result_text.length > chars
37
+ result_text = result_text[0...chars]
38
+ hidden_chars = text.length - chars
39
+ truncation_parts << "#{hidden_chars} more chars"
40
+ truncated = true
41
+ end
42
+
43
+ truncation_msg = truncated ? "... (#{truncation_parts.join(", ")})" : nil
44
+
45
+ [result_text, truncation_msg]
46
+ end
47
+
48
+ # Wrap text to specified width
49
+ def wrap(text, width:)
50
+ return "" if text.nil? || text.empty?
51
+
52
+ text.split("\n").flat_map do |line|
53
+ wrap_line(line, width)
54
+ end.join("\n")
55
+ end
56
+
57
+ # Indent all lines in text
58
+ def indent(text, level: 0, char: " ")
59
+ return "" if text.nil? || text.empty?
60
+
61
+ prefix = char * level
62
+ text.split("\n").map { |line| "#{prefix}#{line}" }.join("\n")
63
+ end
64
+
65
+ private
66
+
67
+ # Word wrap a single line
68
+ def wrap_line(line, width)
69
+ return [line] if line.length <= width
70
+
71
+ line.scan(/.{1,#{width}}(?:\s+|$)/).map(&:strip)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module Formatters
6
+ # Time and duration formatting utilities
7
+ class Time
8
+ class << self
9
+ # Format timestamp as [HH:MM:SS]
10
+ # Time.now → "[12:34:56]"
11
+ def timestamp(time)
12
+ return "" if time.nil?
13
+
14
+ case time
15
+ when ::Time
16
+ time.strftime("[%H:%M:%S]")
17
+ when String
18
+ parsed = ::Time.parse(time)
19
+ parsed.strftime("[%H:%M:%S]")
20
+ else
21
+ ""
22
+ end
23
+ rescue StandardError
24
+ ""
25
+ end
26
+
27
+ # Format duration in human-readable form
28
+ # 0.5 → "500ms"
29
+ # 2.3 → "2.3s"
30
+ # 65 → "1m 5s"
31
+ # 3665 → "1h 1m 5s"
32
+ def duration(seconds)
33
+ return "0ms" if seconds.nil? || seconds.zero?
34
+
35
+ if seconds < 1
36
+ "#{(seconds * 1000).round}ms"
37
+ elsif seconds < 60
38
+ "#{seconds.round(2)}s"
39
+ elsif seconds < 3600
40
+ minutes = (seconds / 60).floor
41
+ secs = (seconds % 60).round
42
+ "#{minutes}m #{secs}s"
43
+ else
44
+ hours = (seconds / 3600).floor
45
+ minutes = ((seconds % 3600) / 60).floor
46
+ secs = (seconds % 60).round
47
+ "#{hours}h #{minutes}m #{secs}s"
48
+ end
49
+ end
50
+
51
+ # Format relative time (future enhancement)
52
+ # Time.now - 120 → "2 minutes ago"
53
+ def relative(time)
54
+ return "" if time.nil?
55
+
56
+ seconds_ago = ::Time.now - time
57
+
58
+ case seconds_ago
59
+ when 0...60
60
+ "#{seconds_ago.round}s ago"
61
+ when 60...3600
62
+ "#{(seconds_ago / 60).round}m ago"
63
+ when 3600...86400
64
+ "#{(seconds_ago / 3600).round}h ago"
65
+ else
66
+ "#{(seconds_ago / 86400).round}d ago"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ # Icon definitions for terminal UI
6
+ # Centralized so all components use the same icons
7
+ module Icons
8
+ # Event type icons
9
+ THINKING = "💭"
10
+ RESPONSE = "💬"
11
+ SUCCESS = "✓"
12
+ ERROR = "✗"
13
+ INFO = "ℹ"
14
+ WARNING = "⚠️"
15
+
16
+ # Entity icons
17
+ AGENT = "🤖"
18
+ TOOL = "🔧"
19
+ DELEGATE = "📨"
20
+ RESULT = "📥"
21
+ HOOK = "🪝"
22
+
23
+ # Metric icons
24
+ LLM = "🧠"
25
+ TOKENS = "📊"
26
+ COST = "💰"
27
+ TIME = "⏱"
28
+
29
+ # Visual elements
30
+ SPARKLES = "✨"
31
+ ARROW_RIGHT = "→"
32
+ BULLET = "•"
33
+ COMPRESS = "🗜️"
34
+
35
+ # All icons as hash for backward compatibility
36
+ ALL = {
37
+ thinking: THINKING,
38
+ response: RESPONSE,
39
+ success: SUCCESS,
40
+ error: ERROR,
41
+ info: INFO,
42
+ warning: WARNING,
43
+ agent: AGENT,
44
+ tool: TOOL,
45
+ delegate: DELEGATE,
46
+ result: RESULT,
47
+ hook: HOOK,
48
+ llm: LLM,
49
+ tokens: TOKENS,
50
+ cost: COST,
51
+ time: TIME,
52
+ sparkles: SPARKLES,
53
+ arrow_right: ARROW_RIGHT,
54
+ bullet: BULLET,
55
+ compress: COMPRESS,
56
+ }.freeze
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module Renderers
6
+ # High-level event rendering by composing lower-level components
7
+ # Returns formatted strings for each event type
8
+ class EventRenderer
9
+ def initialize(pastel:, agent_badge:, depth_tracker:)
10
+ @pastel = pastel
11
+ @agent_badge = agent_badge
12
+ @depth_tracker = depth_tracker
13
+ @usage_stats = Components::UsageStats.new(pastel: pastel)
14
+ @content_block = Components::ContentBlock.new(pastel: pastel)
15
+ @panel = Components::Panel.new(pastel: pastel)
16
+ end
17
+
18
+ # Render agent thinking event
19
+ # [12:34:56] 💭 architect (gpt-5-mini)
20
+ def agent_thinking(agent:, model:, timestamp:)
21
+ indent = @depth_tracker.indent(agent)
22
+ time = Formatters::Time.timestamp(timestamp)
23
+ agent_name = @agent_badge.render(agent, icon: UI::Icons::THINKING)
24
+ model_info = @pastel.dim("(#{model})")
25
+
26
+ "#{indent}#{@pastel.dim(time)} #{agent_name} #{model_info}"
27
+ end
28
+
29
+ # Render agent response event
30
+ # [12:34:56] 💬 architect responded:
31
+ def agent_response(agent:, timestamp:)
32
+ indent = @depth_tracker.indent(agent)
33
+ time = Formatters::Time.timestamp(timestamp)
34
+ agent_name = @agent_badge.render(agent, icon: UI::Icons::RESPONSE)
35
+
36
+ "#{indent}#{@pastel.dim(time)} #{agent_name} responded:"
37
+ end
38
+
39
+ # Render agent completion
40
+ # ✓ architect completed
41
+ def agent_completed(agent:)
42
+ indent = @depth_tracker.indent(agent)
43
+ agent_name = @agent_badge.render(agent)
44
+
45
+ "#{indent}#{@pastel.green("#{UI::Icons::SUCCESS} #{agent_name} completed")}"
46
+ end
47
+
48
+ # Render tool call event
49
+ # [12:34:56] architect 🔧 uses tool Read
50
+ def tool_call(agent:, tool:, timestamp:)
51
+ indent = @depth_tracker.indent(agent)
52
+ time = Formatters::Time.timestamp(timestamp)
53
+ agent_name = @agent_badge.render(agent)
54
+ tool_name = @pastel.bold.blue(tool)
55
+
56
+ "#{indent}#{@pastel.dim(time)} #{agent_name} #{@pastel.blue("#{UI::Icons::TOOL} uses tool")} #{tool_name}"
57
+ end
58
+
59
+ # Render tool result received
60
+ # [12:34:56] 📥 Tool result received by architect
61
+ def tool_result(agent:, timestamp:, tool: nil)
62
+ indent = @depth_tracker.indent(agent)
63
+ time = Formatters::Time.timestamp(timestamp)
64
+
65
+ "#{indent}#{@pastel.dim(time)} #{@pastel.green("#{UI::Icons::RESULT} Tool result")} received by #{agent}"
66
+ end
67
+
68
+ # Render delegation event
69
+ # [12:34:56] architect 📨 delegates to worker
70
+ def delegation(from:, to:, timestamp:)
71
+ indent = @depth_tracker.indent(from)
72
+ time = Formatters::Time.timestamp(timestamp)
73
+ from_name = @agent_badge.render(from)
74
+ to_name = @agent_badge.render(to)
75
+
76
+ "#{indent}#{@pastel.dim(time)} #{from_name} #{@pastel.yellow("#{UI::Icons::DELEGATE} delegates to")} #{to_name}"
77
+ end
78
+
79
+ # Render delegation result
80
+ # [12:34:56] 📥 Delegation result from worker → architect
81
+ def delegation_result(from:, to:, timestamp:)
82
+ indent = @depth_tracker.indent(to)
83
+ time = Formatters::Time.timestamp(timestamp)
84
+ from_name = @agent_badge.render(from)
85
+ to_name = @agent_badge.render(to)
86
+
87
+ "#{indent}#{@pastel.dim(time)} #{@pastel.green("#{UI::Icons::RESULT} Delegation result")} from #{from_name} #{@pastel.dim("→")} #{to_name}"
88
+ end
89
+
90
+ # Render hook execution
91
+ # [12:34:56] 🪝 Hook executed PreToolUse architect
92
+ def hook_executed(hook_event:, agent:, timestamp:, success:, blocked:)
93
+ indent = @depth_tracker.indent(agent)
94
+ time = Formatters::Time.timestamp(timestamp)
95
+ hook_display = @pastel.cyan(hook_event)
96
+ agent_name = @agent_badge.render(agent)
97
+
98
+ status = if blocked
99
+ @pastel.red("BLOCKED")
100
+ elsif success
101
+ @pastel.green("executed")
102
+ else
103
+ @pastel.yellow("warning")
104
+ end
105
+
106
+ color = if blocked
107
+ :red
108
+ else
109
+ (success ? :green : :yellow)
110
+ end
111
+ icon_colored = @pastel.public_send(color, UI::Icons::HOOK)
112
+
113
+ "#{indent}#{@pastel.dim(time)} #{icon_colored} Hook #{status} #{hook_display} #{agent_name}"
114
+ end
115
+
116
+ # Render usage stats line
117
+ # 5,922 tokens │ $0.0016 │ 1.5% used, 394,078 remaining
118
+ def usage_stats(tokens:, cost:, context_pct: nil, remaining: nil, cumulative: nil, indent: 0)
119
+ prefix = " " * indent
120
+ stats = @usage_stats.render(
121
+ tokens: tokens,
122
+ cost: cost,
123
+ context_pct: context_pct,
124
+ remaining: remaining,
125
+ cumulative: cumulative,
126
+ )
127
+
128
+ "#{prefix} #{stats}"
129
+ end
130
+
131
+ # Render tool list
132
+ # Tools available: Read, Write, Bash
133
+ def tools_available(tools, indent: 0)
134
+ return "" if tools.nil? || tools.empty?
135
+
136
+ prefix = " " * indent
137
+ tools_list = tools.join(", ")
138
+
139
+ "#{prefix} #{@pastel.dim("Tools available: #{tools_list}")}"
140
+ end
141
+
142
+ # Render delegation list
143
+ # Can delegate to: frontend_dev, backend_dev
144
+ def delegates_to(agents, indent: 0, color_cache:)
145
+ return "" if agents.nil? || agents.empty?
146
+
147
+ prefix = " " * indent
148
+ agent_badge = Components::AgentBadge.new(pastel: @pastel, color_cache: color_cache)
149
+ delegates_list = agent_badge.render_list(agents)
150
+
151
+ "#{prefix} #{@pastel.dim("Can delegate to:")} #{delegates_list}"
152
+ end
153
+
154
+ # Render thinking text (italic, indented)
155
+ def thinking_text(content, indent: 0)
156
+ return "" if content.nil? || content.empty?
157
+
158
+ # Strip system reminders
159
+ text = Formatters::Text.strip_system_reminders(content)
160
+ return "" if text.empty?
161
+
162
+ prefix = " " * indent
163
+
164
+ text.split("\n").map do |line|
165
+ "#{prefix} #{@pastel.italic(line)}"
166
+ end.join("\n")
167
+ end
168
+
169
+ # Render tool arguments
170
+ def tool_arguments(args, indent: 0, truncate: false)
171
+ @content_block.render_hash(args, indent: indent, label: "Arguments", truncate: truncate)
172
+ end
173
+
174
+ # Render tool result content
175
+ def tool_result_content(content, indent: 0, truncate: false)
176
+ @content_block.render_text(
177
+ content,
178
+ indent: indent,
179
+ color: :bright_green,
180
+ truncate: truncate,
181
+ max_lines: 2,
182
+ max_chars: 300,
183
+ )
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module UI
5
+ module State
6
+ # Caches agent name → color assignments for consistent coloring
7
+ class AgentColorCache
8
+ # Professional color palette inspired by modern CLIs
9
+ PALETTE = [
10
+ :cyan,
11
+ :magenta,
12
+ :yellow,
13
+ :blue,
14
+ :green,
15
+ :bright_cyan,
16
+ :bright_magenta,
17
+ ].freeze
18
+
19
+ def initialize
20
+ @cache = {}
21
+ @next_index = 0
22
+ end
23
+
24
+ # Get color for agent (cached)
25
+ def get(agent_name)
26
+ @cache[agent_name] ||= assign_next_color
27
+ end
28
+
29
+ # Reset cache (for testing)
30
+ def reset
31
+ @cache.clear
32
+ @next_index = 0
33
+ end
34
+
35
+ private
36
+
37
+ def assign_next_color
38
+ color = PALETTE[@next_index % PALETTE.size]
39
+ @next_index += 1
40
+ color
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end