swarm_memory 2.1.4 → 2.1.6

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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. data/lib/swarm_memory.rb +7 -2
  4. metadata +6 -185
  5. data/lib/claude_swarm/base_executor.rb +0 -133
  6. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  7. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  8. data/lib/claude_swarm/cli.rb +0 -697
  9. data/lib/claude_swarm/commands/ps.rb +0 -215
  10. data/lib/claude_swarm/commands/show.rb +0 -139
  11. data/lib/claude_swarm/configuration.rb +0 -373
  12. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  13. data/lib/claude_swarm/json_handler.rb +0 -91
  14. data/lib/claude_swarm/mcp_generator.rb +0 -243
  15. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  16. data/lib/claude_swarm/openai/executor.rb +0 -256
  17. data/lib/claude_swarm/openai/responses.rb +0 -319
  18. data/lib/claude_swarm/orchestrator.rb +0 -878
  19. data/lib/claude_swarm/process_tracker.rb +0 -78
  20. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  21. data/lib/claude_swarm/session_path.rb +0 -42
  22. data/lib/claude_swarm/settings_generator.rb +0 -77
  23. data/lib/claude_swarm/system_utils.rb +0 -46
  24. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  25. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  27. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  28. data/lib/claude_swarm/version.rb +0 -5
  29. data/lib/claude_swarm/worktree_manager.rb +0 -475
  30. data/lib/claude_swarm/yaml_loader.rb +0 -22
  31. data/lib/claude_swarm.rb +0 -67
  32. data/lib/swarm_cli/cli.rb +0 -201
  33. data/lib/swarm_cli/command_registry.rb +0 -61
  34. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  35. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  36. data/lib/swarm_cli/commands/migrate.rb +0 -55
  37. data/lib/swarm_cli/commands/run.rb +0 -173
  38. data/lib/swarm_cli/config_loader.rb +0 -98
  39. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  40. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  41. data/lib/swarm_cli/interactive_repl.rb +0 -924
  42. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  43. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  44. data/lib/swarm_cli/migrate_options.rb +0 -54
  45. data/lib/swarm_cli/migrator.rb +0 -132
  46. data/lib/swarm_cli/options.rb +0 -151
  47. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  48. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  49. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  50. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  51. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  52. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  53. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  54. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  55. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  56. data/lib/swarm_cli/ui/icons.rb +0 -36
  57. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  58. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  59. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  60. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  61. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  62. data/lib/swarm_cli/version.rb +0 -5
  63. data/lib/swarm_cli.rb +0 -46
  64. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  65. data/lib/swarm_sdk/agent/builder.rb +0 -552
  66. data/lib/swarm_sdk/agent/chat.rb +0 -774
  67. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  68. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  69. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  70. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  71. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  72. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  73. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  75. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  76. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  77. data/lib/swarm_sdk/agent/context.rb +0 -116
  78. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  79. data/lib/swarm_sdk/agent/definition.rb +0 -477
  80. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  81. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  82. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  83. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  84. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  85. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  86. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  87. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  88. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  89. data/lib/swarm_sdk/configuration.rb +0 -135
  90. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  91. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  92. data/lib/swarm_sdk/context_compactor.rb +0 -335
  93. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  94. data/lib/swarm_sdk/context_management/context.rb +0 -328
  95. data/lib/swarm_sdk/defaults.rb +0 -196
  96. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  97. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  98. data/lib/swarm_sdk/hooks/context.rb +0 -197
  99. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  100. data/lib/swarm_sdk/hooks/error.rb +0 -29
  101. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  102. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  103. data/lib/swarm_sdk/hooks/result.rb +0 -150
  104. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  105. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  106. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  107. data/lib/swarm_sdk/log_collector.rb +0 -227
  108. data/lib/swarm_sdk/log_stream.rb +0 -127
  109. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  110. data/lib/swarm_sdk/model_aliases.json +0 -8
  111. data/lib/swarm_sdk/models.json +0 -1
  112. data/lib/swarm_sdk/models.rb +0 -120
  113. data/lib/swarm_sdk/node_context.rb +0 -245
  114. data/lib/swarm_sdk/observer/builder.rb +0 -81
  115. data/lib/swarm_sdk/observer/config.rb +0 -45
  116. data/lib/swarm_sdk/observer/manager.rb +0 -236
  117. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  118. data/lib/swarm_sdk/permissions/config.rb +0 -239
  119. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  120. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  121. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  122. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  123. data/lib/swarm_sdk/plugin.rb +0 -309
  124. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  125. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  126. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  127. data/lib/swarm_sdk/restore_result.rb +0 -65
  128. data/lib/swarm_sdk/result.rb +0 -123
  129. data/lib/swarm_sdk/snapshot.rb +0 -156
  130. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  131. data/lib/swarm_sdk/state_restorer.rb +0 -476
  132. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  133. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  134. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  135. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  136. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  137. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  138. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  139. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  140. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  141. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  142. data/lib/swarm_sdk/swarm.rb +0 -717
  143. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  144. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  145. data/lib/swarm_sdk/tools/bash.rb +0 -282
  146. data/lib/swarm_sdk/tools/clock.rb +0 -44
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  160. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  161. data/lib/swarm_sdk/tools/read.rb +0 -261
  162. data/lib/swarm_sdk/tools/registry.rb +0 -205
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  166. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  167. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  168. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  169. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  170. data/lib/swarm_sdk/tools/think.rb +0 -98
  171. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  172. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  173. data/lib/swarm_sdk/tools/write.rb +0 -112
  174. data/lib/swarm_sdk/utils.rb +0 -68
  175. data/lib/swarm_sdk/validation_result.rb +0 -33
  176. data/lib/swarm_sdk/version.rb +0 -5
  177. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  182. data/lib/swarm_sdk/workflow.rb +0 -554
  183. data/lib/swarm_sdk.rb +0 -524
  184. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -1,215 +0,0 @@
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
- root_dir_file = File.join(session_dir, "root_directory")
100
- base_dir = File.exist?(root_dir_file) ? File.read(root_dir_file).strip : Dir.pwd
101
-
102
- # Get all directories - handle both string and array formats
103
- dir_config = config.dig("swarm", "instances", main_instance, "directory")
104
- directories = if dir_config.is_a?(Array)
105
- dir_config
106
- else
107
- [dir_config || "."]
108
- end
109
-
110
- # Expand paths relative to the base directory
111
- expanded_directories = directories.map do |dir|
112
- File.expand_path(dir, base_dir)
113
- end
114
-
115
- # Check for worktree information in session metadata
116
- expanded_directories = apply_worktree_paths(expanded_directories, session_dir)
117
-
118
- directories_str = expanded_directories.join(", ")
119
-
120
- # Calculate total cost from JSON log
121
- log_file = File.join(session_dir, "session.log.json")
122
- cost_result = SessionCostCalculator.calculate_total_cost(log_file)
123
- total_cost = cost_result[:total_cost]
124
-
125
- # Check if main instance has cost data
126
- instances_with_cost = cost_result[:instances_with_cost]
127
- main_has_cost = main_instance && instances_with_cost.include?(main_instance)
128
-
129
- # Get uptime from session metadata or fallback to directory creation time
130
- start_time = get_start_time(session_dir)
131
- uptime = format_duration(Time.now - start_time)
132
-
133
- {
134
- id: session_id,
135
- name: swarm_name,
136
- cost: total_cost,
137
- main_has_cost: main_has_cost,
138
- uptime: uptime,
139
- directory: directories_str,
140
- start_time: start_time,
141
- }
142
- end
143
-
144
- def get_start_time(session_dir)
145
- # Try to get from session metadata first
146
- metadata_file = File.join(session_dir, "session_metadata.json")
147
- metadata = JsonHandler.parse_file(metadata_file)
148
-
149
- if metadata && metadata["start_time"]
150
- return Time.parse(metadata["start_time"])
151
- end
152
-
153
- # Fallback to directory creation time
154
- File.stat(session_dir).ctime
155
- rescue StandardError
156
- # If anything fails, use directory creation time
157
- File.stat(session_dir).ctime
158
- end
159
-
160
- def format_duration(seconds)
161
- if seconds < 60
162
- "#{seconds.to_i}s"
163
- elsif seconds < 3600
164
- "#{(seconds / 60).to_i}m"
165
- elsif seconds < 86_400
166
- "#{(seconds / 3600).to_i}h"
167
- else
168
- "#{(seconds / 86_400).to_i}d"
169
- end
170
- end
171
-
172
- def truncate(str, length)
173
- str.length > length ? "#{str[0...length - 2]}.." : str
174
- end
175
-
176
- def apply_worktree_paths(directories, session_dir)
177
- session_metadata_file = File.join(session_dir, "session_metadata.json")
178
- return directories unless File.exist?(session_metadata_file)
179
-
180
- metadata = JsonHandler.parse_file!(session_metadata_file)
181
- worktree_info = metadata["worktree"]
182
- return directories unless worktree_info && worktree_info["enabled"]
183
-
184
- # Get the created worktree paths
185
- created_paths = worktree_info["created_paths"] || {}
186
-
187
- # For each directory, find the appropriate worktree path
188
- directories.map do |dir|
189
- # Find if this directory has a worktree created
190
- repo_root = find_git_root(dir)
191
- next dir unless repo_root
192
-
193
- # Look for a worktree with this repo root
194
- worktree_key = created_paths.keys.find { |key| key.start_with?("#{repo_root}:") }
195
- worktree_key ? created_paths[worktree_key] : dir
196
- end
197
- end
198
-
199
- def worktree_path_for(dir, worktree_name)
200
- git_root = find_git_root(dir)
201
- git_root ? File.join(git_root, ".worktrees", worktree_name) : dir
202
- end
203
-
204
- def find_git_root(dir)
205
- current = File.expand_path(dir)
206
- while current != "/"
207
- return current if File.exist?(File.join(current, ".git"))
208
-
209
- current = File.dirname(current)
210
- end
211
- nil
212
- end
213
- end
214
- end
215
- end
@@ -1,139 +0,0 @@
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