swarm_memory 2.0.0
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 +7 -0
- data/LICENSE +21 -0
- data/lib/claude_swarm/base_executor.rb +133 -0
- data/lib/claude_swarm/claude_code_executor.rb +349 -0
- data/lib/claude_swarm/claude_mcp_server.rb +77 -0
- data/lib/claude_swarm/cli.rb +712 -0
- data/lib/claude_swarm/commands/ps.rb +216 -0
- data/lib/claude_swarm/commands/show.rb +139 -0
- data/lib/claude_swarm/configuration.rb +363 -0
- data/lib/claude_swarm/hooks/session_start_hook.rb +42 -0
- data/lib/claude_swarm/json_handler.rb +91 -0
- data/lib/claude_swarm/mcp_generator.rb +248 -0
- data/lib/claude_swarm/openai/chat_completion.rb +264 -0
- data/lib/claude_swarm/openai/executor.rb +254 -0
- data/lib/claude_swarm/openai/responses.rb +338 -0
- data/lib/claude_swarm/orchestrator.rb +879 -0
- data/lib/claude_swarm/process_tracker.rb +78 -0
- data/lib/claude_swarm/session_cost_calculator.rb +209 -0
- data/lib/claude_swarm/session_path.rb +42 -0
- data/lib/claude_swarm/settings_generator.rb +77 -0
- data/lib/claude_swarm/system_utils.rb +46 -0
- data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
- data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
- data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
- data/lib/claude_swarm/tools/task_tool.rb +63 -0
- data/lib/claude_swarm/version.rb +5 -0
- data/lib/claude_swarm/worktree_manager.rb +475 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +69 -0
- 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 +45 -0
- data/lib/swarm_memory/adapters/base.rb +140 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +789 -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 +286 -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 +139 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +201 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +76 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +40 -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 +145 -0
- data/lib/swarm_memory/tools/memory_grep.rb +209 -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 +215 -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 +1144 -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 +416 -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 +167 -0
- metadata +313 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
module Commands
|
|
5
|
+
class Ps
|
|
6
|
+
def execute
|
|
7
|
+
run_dir = ClaudeSwarm.joined_run_dir
|
|
8
|
+
unless Dir.exist?(run_dir)
|
|
9
|
+
puts "No active sessions"
|
|
10
|
+
return
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Read all symlinks in run directory and process them
|
|
14
|
+
sessions = Dir.glob("#{run_dir}/*").filter_map do |symlink|
|
|
15
|
+
process_symlink(symlink)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if sessions.empty?
|
|
19
|
+
puts "No active sessions"
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Check if any session is missing main instance costs
|
|
24
|
+
any_missing_main = sessions.any? { |s| !s[:main_has_cost] }
|
|
25
|
+
|
|
26
|
+
# Column widths
|
|
27
|
+
col_session = 15
|
|
28
|
+
col_swarm = 25
|
|
29
|
+
col_cost = 12
|
|
30
|
+
col_uptime = 10
|
|
31
|
+
|
|
32
|
+
# Display header with proper spacing
|
|
33
|
+
header = "#{
|
|
34
|
+
"SESSION_ID".ljust(col_session)
|
|
35
|
+
} #{
|
|
36
|
+
"SWARM_NAME".ljust(col_swarm)
|
|
37
|
+
} #{
|
|
38
|
+
"TOTAL_COST".ljust(col_cost)
|
|
39
|
+
} #{
|
|
40
|
+
"UPTIME".ljust(col_uptime)
|
|
41
|
+
} DIRECTORY"
|
|
42
|
+
|
|
43
|
+
# Only show warning if any session is missing main instance costs
|
|
44
|
+
if any_missing_main
|
|
45
|
+
puts "\n⚠️ \e[3mTotal cost does not include the cost of the main instance for some sessions\e[0m\n\n"
|
|
46
|
+
else
|
|
47
|
+
puts
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
puts header
|
|
51
|
+
puts "-" * header.length
|
|
52
|
+
|
|
53
|
+
# Display sessions sorted by start time (newest first)
|
|
54
|
+
sessions.sort_by { |s| s[:start_time] }.reverse.each do |session|
|
|
55
|
+
cost_str = format("$%.4f", session[:cost])
|
|
56
|
+
# Add asterisk if this session is missing main instance cost
|
|
57
|
+
cost_str += "*" unless session[:main_has_cost]
|
|
58
|
+
|
|
59
|
+
puts "#{
|
|
60
|
+
session[:id].ljust(col_session)
|
|
61
|
+
} #{
|
|
62
|
+
truncate(session[:name], col_swarm).ljust(col_swarm)
|
|
63
|
+
} #{
|
|
64
|
+
cost_str.ljust(col_cost)
|
|
65
|
+
} #{
|
|
66
|
+
session[:uptime].ljust(col_uptime)
|
|
67
|
+
} #{session[:directory]}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def process_symlink(symlink)
|
|
74
|
+
session_dir = File.readlink(symlink)
|
|
75
|
+
session_id = File.basename(session_dir)
|
|
76
|
+
# Skip if target doesn't exist (stale symlink)
|
|
77
|
+
return unless Dir.exist?(session_dir)
|
|
78
|
+
|
|
79
|
+
parse_session_info(session_id, session_dir)
|
|
80
|
+
rescue Errno::EINVAL
|
|
81
|
+
# Not a symlink, skip it
|
|
82
|
+
nil
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
# Try to get session_id if we have session_dir
|
|
85
|
+
warn("⚠️ Skipping session #{session_id}: #{e.message}")
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_session_info(session_id, session_dir)
|
|
90
|
+
# Load config for swarm name and main directory
|
|
91
|
+
config_file = File.join(session_dir, "config.yml")
|
|
92
|
+
return unless File.exist?(config_file)
|
|
93
|
+
|
|
94
|
+
config = YamlLoader.load_config_file(config_file)
|
|
95
|
+
swarm_name = config.dig("swarm", "name") || "Unknown"
|
|
96
|
+
main_instance = config.dig("swarm", "main")
|
|
97
|
+
|
|
98
|
+
# Get base directory from session metadata or root_directory file
|
|
99
|
+
base_dir = ClaudeSwarm.root_dir
|
|
100
|
+
root_dir_file = File.join(session_dir, "root_directory")
|
|
101
|
+
base_dir = File.read(root_dir_file).strip if File.exist?(root_dir_file)
|
|
102
|
+
|
|
103
|
+
# Get all directories - handle both string and array formats
|
|
104
|
+
dir_config = config.dig("swarm", "instances", main_instance, "directory")
|
|
105
|
+
directories = if dir_config.is_a?(Array)
|
|
106
|
+
dir_config
|
|
107
|
+
else
|
|
108
|
+
[dir_config || "."]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Expand paths relative to the base directory
|
|
112
|
+
expanded_directories = directories.map do |dir|
|
|
113
|
+
File.expand_path(dir, base_dir)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check for worktree information in session metadata
|
|
117
|
+
expanded_directories = apply_worktree_paths(expanded_directories, session_dir)
|
|
118
|
+
|
|
119
|
+
directories_str = expanded_directories.join(", ")
|
|
120
|
+
|
|
121
|
+
# Calculate total cost from JSON log
|
|
122
|
+
log_file = File.join(session_dir, "session.log.json")
|
|
123
|
+
cost_result = SessionCostCalculator.calculate_total_cost(log_file)
|
|
124
|
+
total_cost = cost_result[:total_cost]
|
|
125
|
+
|
|
126
|
+
# Check if main instance has cost data
|
|
127
|
+
instances_with_cost = cost_result[:instances_with_cost]
|
|
128
|
+
main_has_cost = main_instance && instances_with_cost.include?(main_instance)
|
|
129
|
+
|
|
130
|
+
# Get uptime from session metadata or fallback to directory creation time
|
|
131
|
+
start_time = get_start_time(session_dir)
|
|
132
|
+
uptime = format_duration(Time.now - start_time)
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
id: session_id,
|
|
136
|
+
name: swarm_name,
|
|
137
|
+
cost: total_cost,
|
|
138
|
+
main_has_cost: main_has_cost,
|
|
139
|
+
uptime: uptime,
|
|
140
|
+
directory: directories_str,
|
|
141
|
+
start_time: start_time,
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def get_start_time(session_dir)
|
|
146
|
+
# Try to get from session metadata first
|
|
147
|
+
metadata_file = File.join(session_dir, "session_metadata.json")
|
|
148
|
+
metadata = JsonHandler.parse_file(metadata_file)
|
|
149
|
+
|
|
150
|
+
if metadata && metadata["start_time"]
|
|
151
|
+
return Time.parse(metadata["start_time"])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Fallback to directory creation time
|
|
155
|
+
File.stat(session_dir).ctime
|
|
156
|
+
rescue StandardError
|
|
157
|
+
# If anything fails, use directory creation time
|
|
158
|
+
File.stat(session_dir).ctime
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def format_duration(seconds)
|
|
162
|
+
if seconds < 60
|
|
163
|
+
"#{seconds.to_i}s"
|
|
164
|
+
elsif seconds < 3600
|
|
165
|
+
"#{(seconds / 60).to_i}m"
|
|
166
|
+
elsif seconds < 86_400
|
|
167
|
+
"#{(seconds / 3600).to_i}h"
|
|
168
|
+
else
|
|
169
|
+
"#{(seconds / 86_400).to_i}d"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def truncate(str, length)
|
|
174
|
+
str.length > length ? "#{str[0...length - 2]}.." : str
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def apply_worktree_paths(directories, session_dir)
|
|
178
|
+
session_metadata_file = File.join(session_dir, "session_metadata.json")
|
|
179
|
+
return directories unless File.exist?(session_metadata_file)
|
|
180
|
+
|
|
181
|
+
metadata = JsonHandler.parse_file!(session_metadata_file)
|
|
182
|
+
worktree_info = metadata["worktree"]
|
|
183
|
+
return directories unless worktree_info && worktree_info["enabled"]
|
|
184
|
+
|
|
185
|
+
# Get the created worktree paths
|
|
186
|
+
created_paths = worktree_info["created_paths"] || {}
|
|
187
|
+
|
|
188
|
+
# For each directory, find the appropriate worktree path
|
|
189
|
+
directories.map do |dir|
|
|
190
|
+
# Find if this directory has a worktree created
|
|
191
|
+
repo_root = find_git_root(dir)
|
|
192
|
+
next dir unless repo_root
|
|
193
|
+
|
|
194
|
+
# Look for a worktree with this repo root
|
|
195
|
+
worktree_key = created_paths.keys.find { |key| key.start_with?("#{repo_root}:") }
|
|
196
|
+
worktree_key ? created_paths[worktree_key] : dir
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def worktree_path_for(dir, worktree_name)
|
|
201
|
+
git_root = find_git_root(dir)
|
|
202
|
+
git_root ? File.join(git_root, ".worktrees", worktree_name) : dir
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def find_git_root(dir)
|
|
206
|
+
current = File.expand_path(dir)
|
|
207
|
+
while current != "/"
|
|
208
|
+
return current if File.exist?(File.join(current, ".git"))
|
|
209
|
+
|
|
210
|
+
current = File.dirname(current)
|
|
211
|
+
end
|
|
212
|
+
nil
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
module Commands
|
|
5
|
+
class Show
|
|
6
|
+
def execute(session_id)
|
|
7
|
+
session_path = find_session_path(session_id)
|
|
8
|
+
unless session_path
|
|
9
|
+
puts "Session not found: #{session_id}"
|
|
10
|
+
exit(1)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Load config to get main instance name
|
|
14
|
+
config = YamlLoader.load_config_file(File.join(session_path, "config.yml"))
|
|
15
|
+
main_instance_name = config.dig("swarm", "main")
|
|
16
|
+
|
|
17
|
+
# Parse all events to build instance data
|
|
18
|
+
log_file = File.join(session_path, "session.log.json")
|
|
19
|
+
instances = SessionCostCalculator.parse_instance_hierarchy(log_file)
|
|
20
|
+
|
|
21
|
+
# Calculate total cost (excluding main if not available)
|
|
22
|
+
total_cost = instances.values.sum { |i| i[:cost] }
|
|
23
|
+
cost_display = if instances[main_instance_name] && instances[main_instance_name][:has_cost_data]
|
|
24
|
+
format("$%.4f", total_cost)
|
|
25
|
+
else
|
|
26
|
+
"#{format("$%.4f", total_cost)} (excluding main instance)"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Display session info
|
|
30
|
+
puts "Session: #{session_id}"
|
|
31
|
+
puts "Session Path: #{session_path}"
|
|
32
|
+
puts "Swarm: #{config.dig("swarm", "name")}"
|
|
33
|
+
|
|
34
|
+
# Display runtime if available
|
|
35
|
+
runtime_info = get_runtime_info(session_path)
|
|
36
|
+
puts "Runtime: #{runtime_info}" if runtime_info
|
|
37
|
+
|
|
38
|
+
puts "Total Cost: #{cost_display}"
|
|
39
|
+
|
|
40
|
+
# Try to read root directory
|
|
41
|
+
root_dir_file = File.join(session_path, "root_directory")
|
|
42
|
+
puts "Root Directory: #{File.read(root_dir_file).strip}" if File.exist?(root_dir_file)
|
|
43
|
+
|
|
44
|
+
puts
|
|
45
|
+
puts "Instance Hierarchy:"
|
|
46
|
+
puts "-" * 50
|
|
47
|
+
|
|
48
|
+
# Find root instances
|
|
49
|
+
roots = instances.values.select { |i| i[:called_by].empty? }
|
|
50
|
+
roots.each do |instance|
|
|
51
|
+
display_instance_tree(instance, instances, 0, main_instance_name)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Add note about interactive main instance
|
|
55
|
+
return if instances[main_instance_name]&.dig(:has_cost_data)
|
|
56
|
+
|
|
57
|
+
puts
|
|
58
|
+
puts "Note: Main instance (#{main_instance_name}) cost is not tracked in interactive mode."
|
|
59
|
+
puts " View costs directly in the Claude interface."
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def find_session_path(session_id)
|
|
65
|
+
# First check the run directory
|
|
66
|
+
run_symlink = ClaudeSwarm.joined_run_dir(session_id)
|
|
67
|
+
if File.symlink?(run_symlink)
|
|
68
|
+
target = File.readlink(run_symlink)
|
|
69
|
+
return target if Dir.exist?(target)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Fall back to searching all sessions
|
|
73
|
+
Dir.glob(ClaudeSwarm.joined_sessions_dir("*", "*")).find do |path|
|
|
74
|
+
File.basename(path) == session_id
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def get_runtime_info(session_path)
|
|
79
|
+
metadata_file = File.join(session_path, "session_metadata.json")
|
|
80
|
+
metadata = JsonHandler.parse_file(metadata_file)
|
|
81
|
+
return unless metadata
|
|
82
|
+
|
|
83
|
+
if metadata["duration_seconds"]
|
|
84
|
+
# Session has completed
|
|
85
|
+
format_duration(metadata["duration_seconds"])
|
|
86
|
+
elsif metadata["start_time"]
|
|
87
|
+
# Session is still running or was interrupted
|
|
88
|
+
start_time = Time.parse(metadata["start_time"])
|
|
89
|
+
duration = (Time.now - start_time).to_i
|
|
90
|
+
"#{format_duration(duration)} (active)"
|
|
91
|
+
end
|
|
92
|
+
rescue StandardError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def format_duration(seconds)
|
|
97
|
+
hours = seconds / 3600
|
|
98
|
+
minutes = (seconds % 3600) / 60
|
|
99
|
+
secs = seconds % 60
|
|
100
|
+
|
|
101
|
+
parts = []
|
|
102
|
+
parts << "#{hours}h" if hours.positive?
|
|
103
|
+
parts << "#{minutes}m" if minutes.positive?
|
|
104
|
+
parts << "#{secs}s"
|
|
105
|
+
|
|
106
|
+
parts.join(" ")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def display_instance_tree(instance, all_instances, level, main_instance_name)
|
|
110
|
+
indent = " " * level
|
|
111
|
+
prefix = level.zero? ? "├─" : "└─"
|
|
112
|
+
|
|
113
|
+
# Display instance name with special marker for main
|
|
114
|
+
instance_display = instance[:name]
|
|
115
|
+
instance_display += " [main]" if instance[:name] == main_instance_name
|
|
116
|
+
|
|
117
|
+
puts "#{indent}#{prefix} #{instance_display} (#{instance[:id]})"
|
|
118
|
+
|
|
119
|
+
# Display cost - show n/a for main instance without cost data
|
|
120
|
+
cost_display = if instance[:name] == main_instance_name && !instance[:has_cost_data]
|
|
121
|
+
"n/a (interactive)"
|
|
122
|
+
else
|
|
123
|
+
format("$%.4f", instance[:cost])
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
puts "#{indent} Cost: #{cost_display} | Calls: #{instance[:calls]}"
|
|
127
|
+
|
|
128
|
+
# Display children
|
|
129
|
+
children = instance[:calls_to].map { |name| all_instances[name] }.compact
|
|
130
|
+
children.each do |child|
|
|
131
|
+
# Don't recurse if we've already shown this instance (avoid cycles)
|
|
132
|
+
next if level.positive? && child[:called_by].size > 1
|
|
133
|
+
|
|
134
|
+
display_instance_tree(child, all_instances, level + 1, main_instance_name)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|