claude_swarm 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/commands/release.md +1 -1
- data/.claude/hooks/lint-code-files.rb +65 -0
- data/.rubocop.yml +22 -2
- data/CHANGELOG.md +14 -1
- data/CLAUDE.md +1 -1
- data/CONTRIBUTING.md +69 -0
- data/README.md +27 -2
- data/Rakefile +71 -3
- data/analyze_coverage.rb +94 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
- data/docs/v2/README.md +308 -0
- data/docs/v2/guides/claude-code-agents.md +262 -0
- data/docs/v2/guides/complete-tutorial.md +3088 -0
- data/docs/v2/guides/getting-started.md +1456 -0
- data/docs/v2/guides/memory-adapters.md +998 -0
- data/docs/v2/guides/plugins.md +816 -0
- data/docs/v2/guides/quick-start-cli.md +1745 -0
- data/docs/v2/guides/rails-integration.md +1902 -0
- data/docs/v2/guides/swarm-memory.md +599 -0
- data/docs/v2/reference/cli.md +729 -0
- data/docs/v2/reference/ruby-dsl.md +2154 -0
- data/docs/v2/reference/yaml.md +1835 -0
- data/docs-team-swarm.yml +2222 -0
- data/examples/learning-assistant/assistant.md +7 -0
- data/examples/learning-assistant/example-memories/concept-example.md +90 -0
- data/examples/learning-assistant/example-memories/experience-example.md +66 -0
- data/examples/learning-assistant/example-memories/fact-example.md +76 -0
- data/examples/learning-assistant/example-memories/memory-index.md +78 -0
- data/examples/learning-assistant/example-memories/skill-example.md +168 -0
- data/examples/learning-assistant/learning_assistant.rb +34 -0
- data/examples/learning-assistant/learning_assistant.yml +20 -0
- data/examples/v2/dsl/01_basic.rb +44 -0
- data/examples/v2/dsl/02_core_parameters.rb +59 -0
- data/examples/v2/dsl/03_capabilities.rb +71 -0
- data/examples/v2/dsl/04_llm_parameters.rb +56 -0
- data/examples/v2/dsl/05_advanced_flags.rb +73 -0
- data/examples/v2/dsl/06_permissions.rb +80 -0
- data/examples/v2/dsl/07_mcp_server.rb +62 -0
- data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
- data/examples/v2/dsl/09_agent_hooks.rb +67 -0
- data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
- data/examples/v2/dsl/11_delegation.rb +60 -0
- data/examples/v2/dsl/12_complete_integration.rb +137 -0
- data/examples/v2/file_tools_swarm.yml +102 -0
- data/examples/v2/hooks/01_basic_hooks.rb +133 -0
- data/examples/v2/hooks/02_usage_tracking.rb +201 -0
- data/examples/v2/hooks/03_production_monitoring.rb +429 -0
- data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
- data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
- data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
- data/examples/v2/hooks/swarm_summary.sh +44 -0
- data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
- data/examples/v2/hooks/validate_bash.rb +59 -0
- data/examples/v2/multi_directory_permissions.yml +221 -0
- data/examples/v2/node_context_demo.rb +127 -0
- data/examples/v2/node_workflow.rb +173 -0
- data/examples/v2/path_resolution_demo.rb +216 -0
- data/examples/v2/simple-swarm-v2.rb +90 -0
- data/examples/v2/simple-swarm-v2.yml +62 -0
- data/examples/v2/swarm.yml +71 -0
- data/examples/v2/swarm_with_hooks.yml +61 -0
- data/examples/v2/swarm_with_hooks_simple.yml +25 -0
- data/examples/v2/think_tool_demo.rb +62 -0
- data/exe/swarm +6 -0
- data/lib/claude_swarm/claude_mcp_server.rb +0 -6
- data/lib/claude_swarm/cli.rb +10 -3
- data/lib/claude_swarm/commands/ps.rb +19 -20
- data/lib/claude_swarm/commands/show.rb +1 -1
- data/lib/claude_swarm/configuration.rb +10 -12
- data/lib/claude_swarm/mcp_generator.rb +10 -1
- data/lib/claude_swarm/orchestrator.rb +73 -49
- data/lib/claude_swarm/system_utils.rb +37 -11
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +1 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +7 -2
- data/lib/swarm_cli/cli.rb +201 -0
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
- data/lib/swarm_cli/commands/migrate.rb +55 -0
- data/lib/swarm_cli/commands/run.rb +173 -0
- data/lib/swarm_cli/config_loader.rb +97 -0
- data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
- data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
- data/lib/swarm_cli/interactive_repl.rb +918 -0
- data/lib/swarm_cli/mcp_serve_options.rb +44 -0
- data/lib/swarm_cli/mcp_tools_options.rb +59 -0
- data/lib/swarm_cli/migrate_options.rb +54 -0
- data/lib/swarm_cli/migrator.rb +132 -0
- data/lib/swarm_cli/options.rb +151 -0
- data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
- data/lib/swarm_cli/ui/components/content_block.rb +120 -0
- data/lib/swarm_cli/ui/components/divider.rb +57 -0
- data/lib/swarm_cli/ui/components/panel.rb +62 -0
- data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
- data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
- data/lib/swarm_cli/ui/formatters/number.rb +58 -0
- data/lib/swarm_cli/ui/formatters/text.rb +77 -0
- data/lib/swarm_cli/ui/formatters/time.rb +73 -0
- data/lib/swarm_cli/ui/icons.rb +59 -0
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
- data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
- data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
- data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
- data/lib/swarm_cli/version.rb +5 -0
- data/lib/swarm_cli.rb +44 -0
- data/lib/swarm_memory/adapters/base.rb +141 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
- data/lib/swarm_memory/chat_extension.rb +34 -0
- data/lib/swarm_memory/cli/commands.rb +306 -0
- data/lib/swarm_memory/core/entry.rb +37 -0
- data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
- data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
- data/lib/swarm_memory/core/path_normalizer.rb +75 -0
- data/lib/swarm_memory/core/semantic_index.rb +244 -0
- data/lib/swarm_memory/core/storage.rb +288 -0
- data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
- data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
- data/lib/swarm_memory/dsl/memory_config.rb +113 -0
- data/lib/swarm_memory/embeddings/embedder.rb +36 -0
- data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
- data/lib/swarm_memory/errors.rb +21 -0
- data/lib/swarm_memory/integration/cli_registration.rb +30 -0
- data/lib/swarm_memory/integration/configuration.rb +43 -0
- data/lib/swarm_memory/integration/registration.rb +31 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
- data/lib/swarm_memory/optimization/analyzer.rb +244 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
- data/lib/swarm_memory/prompts/memory.md.erb +109 -0
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +42 -0
- data/lib/swarm_memory/search/text_similarity.rb +80 -0
- data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
- data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
- data/lib/swarm_memory/tools/load_skill.rb +313 -0
- data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
- data/lib/swarm_memory/tools/memory_delete.rb +99 -0
- data/lib/swarm_memory/tools/memory_edit.rb +185 -0
- data/lib/swarm_memory/tools/memory_glob.rb +160 -0
- data/lib/swarm_memory/tools/memory_grep.rb +247 -0
- data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
- data/lib/swarm_memory/tools/memory_read.rb +123 -0
- data/lib/swarm_memory/tools/memory_write.rb +231 -0
- data/lib/swarm_memory/utils.rb +50 -0
- data/lib/swarm_memory/version.rb +5 -0
- data/lib/swarm_memory.rb +166 -0
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +461 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
- data/lib/swarm_sdk/agent/chat.rb +1159 -0
- data/lib/swarm_sdk/agent/context.rb +112 -0
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +556 -0
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +296 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +197 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +147 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +51 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +75 -0
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
- data/lib/swarm_sdk/swarm/builder.rb +586 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
- data/lib/swarm_sdk/swarm.rb +982 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/delegate.rb +164 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +228 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +93 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +157 -0
- data/llm.v2.txt +13407 -0
- data/rubocop/cop/security/no_reflection_methods.rb +47 -0
- data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
- data/swarm_cli.gemspec +57 -0
- data/swarm_memory.gemspec +28 -0
- data/swarm_sdk.gemspec +41 -0
- data/team.yml +1 -1
- data/team_full.yml +1875 -0
- data/{team_v2.yml → team_sdk.yml} +121 -52
- metadata +249 -6
- 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
|
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
|