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.
Files changed (189) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/lib/claude_swarm/base_executor.rb +133 -0
  4. data/lib/claude_swarm/claude_code_executor.rb +349 -0
  5. data/lib/claude_swarm/claude_mcp_server.rb +77 -0
  6. data/lib/claude_swarm/cli.rb +712 -0
  7. data/lib/claude_swarm/commands/ps.rb +216 -0
  8. data/lib/claude_swarm/commands/show.rb +139 -0
  9. data/lib/claude_swarm/configuration.rb +363 -0
  10. data/lib/claude_swarm/hooks/session_start_hook.rb +42 -0
  11. data/lib/claude_swarm/json_handler.rb +91 -0
  12. data/lib/claude_swarm/mcp_generator.rb +248 -0
  13. data/lib/claude_swarm/openai/chat_completion.rb +264 -0
  14. data/lib/claude_swarm/openai/executor.rb +254 -0
  15. data/lib/claude_swarm/openai/responses.rb +338 -0
  16. data/lib/claude_swarm/orchestrator.rb +879 -0
  17. data/lib/claude_swarm/process_tracker.rb +78 -0
  18. data/lib/claude_swarm/session_cost_calculator.rb +209 -0
  19. data/lib/claude_swarm/session_path.rb +42 -0
  20. data/lib/claude_swarm/settings_generator.rb +77 -0
  21. data/lib/claude_swarm/system_utils.rb +46 -0
  22. data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
  23. data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
  24. data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
  25. data/lib/claude_swarm/tools/task_tool.rb +63 -0
  26. data/lib/claude_swarm/version.rb +5 -0
  27. data/lib/claude_swarm/worktree_manager.rb +475 -0
  28. data/lib/claude_swarm/yaml_loader.rb +22 -0
  29. data/lib/claude_swarm.rb +69 -0
  30. data/lib/swarm_cli/cli.rb +201 -0
  31. data/lib/swarm_cli/command_registry.rb +61 -0
  32. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  33. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  34. data/lib/swarm_cli/commands/migrate.rb +55 -0
  35. data/lib/swarm_cli/commands/run.rb +173 -0
  36. data/lib/swarm_cli/config_loader.rb +97 -0
  37. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  38. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  39. data/lib/swarm_cli/interactive_repl.rb +918 -0
  40. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  41. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  42. data/lib/swarm_cli/migrate_options.rb +54 -0
  43. data/lib/swarm_cli/migrator.rb +132 -0
  44. data/lib/swarm_cli/options.rb +151 -0
  45. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  46. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  47. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  48. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  49. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  50. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  51. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  52. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  53. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  54. data/lib/swarm_cli/ui/icons.rb +59 -0
  55. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  56. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  57. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  58. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  59. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  60. data/lib/swarm_cli/version.rb +5 -0
  61. data/lib/swarm_cli.rb +45 -0
  62. data/lib/swarm_memory/adapters/base.rb +140 -0
  63. data/lib/swarm_memory/adapters/filesystem_adapter.rb +789 -0
  64. data/lib/swarm_memory/chat_extension.rb +34 -0
  65. data/lib/swarm_memory/cli/commands.rb +306 -0
  66. data/lib/swarm_memory/core/entry.rb +37 -0
  67. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  68. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  69. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  70. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  71. data/lib/swarm_memory/core/storage.rb +286 -0
  72. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  73. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  74. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  75. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  76. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  77. data/lib/swarm_memory/errors.rb +21 -0
  78. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  79. data/lib/swarm_memory/integration/configuration.rb +43 -0
  80. data/lib/swarm_memory/integration/registration.rb +31 -0
  81. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  82. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  83. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  84. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  85. data/lib/swarm_memory/prompts/memory_assistant.md.erb +139 -0
  86. data/lib/swarm_memory/prompts/memory_researcher.md.erb +201 -0
  87. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +76 -0
  88. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  89. data/lib/swarm_memory/search/text_search.rb +40 -0
  90. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  91. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  92. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  93. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  94. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  95. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  96. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  97. data/lib/swarm_memory/tools/memory_glob.rb +145 -0
  98. data/lib/swarm_memory/tools/memory_grep.rb +209 -0
  99. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  100. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  101. data/lib/swarm_memory/tools/memory_write.rb +215 -0
  102. data/lib/swarm_memory/utils.rb +50 -0
  103. data/lib/swarm_memory/version.rb +5 -0
  104. data/lib/swarm_memory.rb +166 -0
  105. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  106. data/lib/swarm_sdk/agent/builder.rb +461 -0
  107. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  108. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  109. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  110. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  111. data/lib/swarm_sdk/agent/chat.rb +1144 -0
  112. data/lib/swarm_sdk/agent/context.rb +112 -0
  113. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  114. data/lib/swarm_sdk/agent/definition.rb +556 -0
  115. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  116. data/lib/swarm_sdk/configuration.rb +296 -0
  117. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  118. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  119. data/lib/swarm_sdk/context_compactor.rb +340 -0
  120. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  121. data/lib/swarm_sdk/hooks/context.rb +197 -0
  122. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  123. data/lib/swarm_sdk/hooks/error.rb +29 -0
  124. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  125. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  126. data/lib/swarm_sdk/hooks/result.rb +150 -0
  127. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  128. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  129. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  130. data/lib/swarm_sdk/log_collector.rb +51 -0
  131. data/lib/swarm_sdk/log_stream.rb +69 -0
  132. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  133. data/lib/swarm_sdk/model_aliases.json +5 -0
  134. data/lib/swarm_sdk/models.json +1 -0
  135. data/lib/swarm_sdk/models.rb +120 -0
  136. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  137. data/lib/swarm_sdk/node/builder.rb +439 -0
  138. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  139. data/lib/swarm_sdk/node_context.rb +170 -0
  140. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  141. data/lib/swarm_sdk/permissions/config.rb +239 -0
  142. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  143. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  144. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  145. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  146. data/lib/swarm_sdk/plugin.rb +147 -0
  147. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  148. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  149. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  150. data/lib/swarm_sdk/result.rb +97 -0
  151. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  152. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  153. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  154. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  155. data/lib/swarm_sdk/swarm/tool_configurator.rb +416 -0
  156. data/lib/swarm_sdk/swarm.rb +982 -0
  157. data/lib/swarm_sdk/tools/bash.rb +274 -0
  158. data/lib/swarm_sdk/tools/clock.rb +44 -0
  159. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  160. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  161. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  162. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  163. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  164. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  165. data/lib/swarm_sdk/tools/edit.rb +150 -0
  166. data/lib/swarm_sdk/tools/glob.rb +158 -0
  167. data/lib/swarm_sdk/tools/grep.rb +228 -0
  168. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  169. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  170. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  171. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  172. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  173. data/lib/swarm_sdk/tools/read.rb +251 -0
  174. data/lib/swarm_sdk/tools/registry.rb +93 -0
  175. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  176. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  177. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  178. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  179. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  180. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  181. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  182. data/lib/swarm_sdk/tools/think.rb +95 -0
  183. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  184. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  185. data/lib/swarm_sdk/tools/write.rb +117 -0
  186. data/lib/swarm_sdk/utils.rb +50 -0
  187. data/lib/swarm_sdk/version.rb +5 -0
  188. data/lib/swarm_sdk.rb +167 -0
  189. 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