prompt_objects 0.5.0 → 0.7.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 (292) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +142 -0
  3. data/README.md +211 -19
  4. data/exe/poop +5 -0
  5. data/exe/prompt_objects +1 -1108
  6. data/exe/prompt_objects_mcp +5 -3
  7. data/lib/prompt_objects/capability.rb +33 -0
  8. data/lib/prompt_objects/cli/diagnostics_command.rb +69 -0
  9. data/lib/prompt_objects/cli/env_command.rb +435 -0
  10. data/lib/prompt_objects/cli/events_command.rb +145 -0
  11. data/lib/prompt_objects/cli/explore_command.rb +155 -0
  12. data/lib/prompt_objects/cli/helpers.rb +91 -0
  13. data/lib/prompt_objects/cli/message_command.rb +216 -0
  14. data/lib/prompt_objects/cli/repl_command.rb +366 -0
  15. data/lib/prompt_objects/cli/serve_command.rb +162 -0
  16. data/lib/prompt_objects/cli.rb +64 -422
  17. data/lib/prompt_objects/connectors/base.rb +16 -29
  18. data/lib/prompt_objects/connectors/mcp.rb +82 -98
  19. data/lib/prompt_objects/domain/records.rb +45 -0
  20. data/lib/prompt_objects/environment/exporter.rb +5 -4
  21. data/lib/prompt_objects/environment/git.rb +21 -1
  22. data/lib/prompt_objects/environment/importer.rb +5 -4
  23. data/lib/prompt_objects/environment/manager.rb +44 -6
  24. data/lib/prompt_objects/environment/manifest.rb +26 -4
  25. data/lib/prompt_objects/environment.rb +1350 -18
  26. data/lib/prompt_objects/execution/blocking_executor.rb +88 -0
  27. data/lib/prompt_objects/execution/context.rb +137 -0
  28. data/lib/prompt_objects/execution/contracts.rb +59 -0
  29. data/lib/prompt_objects/execution/permit_pool.rb +49 -0
  30. data/lib/prompt_objects/execution/policy.rb +47 -0
  31. data/lib/prompt_objects/execution/resource_lock_manager.rb +63 -0
  32. data/lib/prompt_objects/execution/runner.rb +498 -0
  33. data/lib/prompt_objects/execution/scheduler.rb +325 -0
  34. data/lib/prompt_objects/llm/factory.rb +4 -4
  35. data/lib/prompt_objects/llm/gemini_adapter.rb +25 -8
  36. data/lib/prompt_objects/llm/openai_adapter.rb +9 -1
  37. data/lib/prompt_objects/llm/pricing.rb +9 -2
  38. data/lib/prompt_objects/llm/response.rb +9 -4
  39. data/lib/prompt_objects/loader.rb +28 -1
  40. data/lib/prompt_objects/mcp/server.rb +6 -3
  41. data/lib/prompt_objects/mcp/tools/get_conversation.rb +15 -11
  42. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +2 -2
  43. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +3 -3
  44. data/lib/prompt_objects/mcp/tools/send_message.rb +42 -32
  45. data/lib/prompt_objects/operations/diagnostics.rb +115 -0
  46. data/lib/prompt_objects/operations/retention.rb +75 -0
  47. data/lib/prompt_objects/operations/run_trace_builder.rb +152 -0
  48. data/lib/prompt_objects/primitives/http_get.rb +2 -0
  49. data/lib/prompt_objects/primitives/list_files.rb +2 -0
  50. data/lib/prompt_objects/primitives/read_file.rb +2 -0
  51. data/lib/prompt_objects/primitives/think.rb +36 -0
  52. data/lib/prompt_objects/primitives/write_file.rb +9 -0
  53. data/lib/prompt_objects/prompt_object.rb +190 -432
  54. data/lib/prompt_objects/repositories/artifact_repository.rb +109 -0
  55. data/lib/prompt_objects/repositories/base_repository.rb +37 -0
  56. data/lib/prompt_objects/repositories/definition_repository.rb +36 -0
  57. data/lib/prompt_objects/repositories/env_data_repository.rb +103 -0
  58. data/lib/prompt_objects/repositories/event_repository.rb +105 -0
  59. data/lib/prompt_objects/repositories/message_repository.rb +73 -0
  60. data/lib/prompt_objects/repositories/run_repository.rb +254 -0
  61. data/lib/prompt_objects/repositories/thread_repository.rb +87 -0
  62. data/lib/prompt_objects/repositories/tool_execution_repository.rb +88 -0
  63. data/lib/prompt_objects/repositories/workspace_session_repository.rb +61 -0
  64. data/lib/prompt_objects/server/api/routes.rb +561 -52
  65. data/lib/prompt_objects/server/app.rb +134 -1
  66. data/lib/prompt_objects/server/file_watcher.rb +9 -0
  67. data/lib/prompt_objects/server/public/assets/index-BjKlODDD.js +90 -0
  68. data/lib/prompt_objects/server/public/assets/index-w3fpIb5c.css +1 -0
  69. data/lib/prompt_objects/server/public/index.html +2 -2
  70. data/lib/prompt_objects/server/websocket_handler.rb +202 -207
  71. data/lib/prompt_objects/server.rb +170 -1
  72. data/lib/prompt_objects/service.rb +129 -0
  73. data/lib/prompt_objects/session/store.rb +774 -15
  74. data/lib/prompt_objects/universal/add_capability.rb +1 -1
  75. data/lib/prompt_objects/universal/create_capability.rb +4 -2
  76. data/lib/prompt_objects/universal/delete_env_data.rb +78 -0
  77. data/lib/prompt_objects/universal/delete_primitive.rb +1 -1
  78. data/lib/prompt_objects/universal/dispatch_po.rb +54 -0
  79. data/lib/prompt_objects/universal/get_env_data.rb +58 -0
  80. data/lib/prompt_objects/universal/get_run_status.rb +30 -0
  81. data/lib/prompt_objects/universal/join_runs.rb +35 -0
  82. data/lib/prompt_objects/universal/list_env_data.rb +57 -0
  83. data/lib/prompt_objects/universal/modify_primitive.rb +1 -1
  84. data/lib/prompt_objects/universal/render_artifact.rb +91 -0
  85. data/lib/prompt_objects/universal/request_primitive.rb +1 -1
  86. data/lib/prompt_objects/universal/set_env_data.rb +101 -0
  87. data/lib/prompt_objects/version.rb +5 -0
  88. data/lib/prompt_objects.rb +36 -6
  89. data/objects/coordinator.md +8 -4
  90. data/templates/arc-agi-1/objects/data_manager.md +2 -2
  91. data/templates/arc-agi-1/objects/observer.md +6 -2
  92. data/templates/arc-agi-1/objects/solver.md +14 -4
  93. data/templates/arc-agi-1/objects/verifier.md +6 -2
  94. data/templates/arc-agi-3/manifest.yml +19 -0
  95. data/templates/arc-agi-3/objects/analyst.md +79 -0
  96. data/templates/arc-agi-3/objects/player.md +76 -0
  97. data/templates/arc-agi-3/objects/strategist.md +97 -0
  98. data/templates/arc-agi-3/primitives/action_log.rb +133 -0
  99. data/templates/arc-agi-3/primitives/arc3_api.rb +223 -0
  100. data/templates/arc-agi-3/primitives/arc3_scorecard.rb +131 -0
  101. data/templates/arc-agi-3/primitives/frame_diff.rb +143 -0
  102. data/templates/arc-agi-3/primitives/frame_info.rb +96 -0
  103. data/templates/arc-agi-3/primitives/render_frame.rb +140 -0
  104. data/templates/basic/objects/basic.md +11 -3
  105. data/templates/chiptune/extras/sonic_pi_boot.rb +220 -0
  106. data/templates/chiptune/manifest.yml +20 -0
  107. data/templates/chiptune/objects/conductor.md +184 -0
  108. data/templates/chiptune/objects/sonic_pi.md +65 -0
  109. data/templates/chiptune/objects/voice_bass.md +44 -0
  110. data/templates/chiptune/objects/voice_lead.md +44 -0
  111. data/templates/chiptune/objects/voice_pad.md +43 -0
  112. data/templates/chiptune/objects/voice_perc.md +45 -0
  113. data/templates/chiptune/primitives/play_note.rb +59 -0
  114. data/templates/chiptune/primitives/play_sequence.rb +55 -0
  115. data/templates/chiptune/primitives/query_sonic_pi.rb +141 -0
  116. data/templates/chiptune/primitives/set_synth.rb +57 -0
  117. data/templates/chiptune/primitives/set_tempo.rb +45 -0
  118. data/templates/chiptune/primitives/stop_voice.rb +46 -0
  119. data/templates/chiptune/primitives/support/osc_bridge.rb +33 -0
  120. data/templates/chiptune/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  121. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  122. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  123. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  124. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  125. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  126. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  127. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  128. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  129. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  130. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  131. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  132. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  133. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  134. data/templates/developer/objects/code_reviewer.md +1 -1
  135. data/templates/developer/objects/coordinator.md +16 -3
  136. data/templates/developer/objects/debugger.md +3 -3
  137. data/templates/empty/objects/assistant.md +10 -5
  138. data/templates/minimal/objects/assistant.md +10 -5
  139. data/templates/music/extras/sonic_pi_boot.rb +220 -0
  140. data/templates/music/manifest.yml +16 -0
  141. data/templates/music/objects/conductor.md +154 -0
  142. data/templates/music/objects/sonic_pi.md +65 -0
  143. data/templates/music/primitives/play_note.rb +55 -0
  144. data/templates/music/primitives/play_sequence.rb +51 -0
  145. data/templates/music/primitives/query_sonic_pi.rb +139 -0
  146. data/templates/music/primitives/set_synth.rb +53 -0
  147. data/templates/music/primitives/set_tempo.rb +43 -0
  148. data/templates/music/primitives/stop_voice.rb +44 -0
  149. data/templates/music/primitives/support/osc_bridge.rb +33 -0
  150. data/templates/music/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  151. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  152. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  153. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  154. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  155. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  156. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  157. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  158. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  159. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  160. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  161. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  162. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  163. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  164. data/templates/music-beginner/README.md +100 -0
  165. data/templates/music-beginner/extras/README.md +27 -0
  166. data/templates/music-beginner/extras/sonic_pi_boot.rb +297 -0
  167. data/templates/music-beginner/manifest.yml +25 -0
  168. data/templates/music-beginner/objects/conductor.md +222 -0
  169. data/templates/music-beginner/objects/engine_setup.md +54 -0
  170. data/templates/music-beginner/objects/voice_bass.md +43 -0
  171. data/templates/music-beginner/objects/voice_keys.md +48 -0
  172. data/templates/music-beginner/objects/voice_pad.md +46 -0
  173. data/templates/music-beginner/objects/voice_percussion.md +45 -0
  174. data/templates/music-beginner/objects/voice_texture.md +37 -0
  175. data/templates/music-beginner/primitives/play_pattern.rb +118 -0
  176. data/templates/music-beginner/primitives/query_music_engine.rb +94 -0
  177. data/templates/music-beginner/primitives/set_instrument.rb +47 -0
  178. data/templates/music-beginner/primitives/set_tempo.rb +43 -0
  179. data/templates/music-beginner/primitives/set_voice_level.rb +46 -0
  180. data/templates/music-beginner/primitives/stop_voice.rb +37 -0
  181. data/templates/music-beginner/primitives/support/osc_bridge.rb +25 -0
  182. data/templates/music-beginner/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  183. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  184. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  185. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  186. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  187. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  188. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  189. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  190. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  191. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  192. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  193. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  194. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  195. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  196. data/templates/music-pro/extras/AUDITION.md +100 -0
  197. data/templates/music-pro/extras/sonic_pi_boot.rb +546 -0
  198. data/templates/music-pro/manifest.yml +19 -0
  199. data/templates/music-pro/objects/conductor.md +126 -0
  200. data/templates/music-pro/objects/sonic_pi.md +35 -0
  201. data/templates/music-pro/objects/voice_harmony.md +48 -0
  202. data/templates/music-pro/objects/voice_rhythm.md +43 -0
  203. data/templates/music-pro/objects/voice_texture.md +42 -0
  204. data/templates/music-pro/primitives/define_patch.rb +46 -0
  205. data/templates/music-pro/primitives/query_music_engine.rb +69 -0
  206. data/templates/music-pro/primitives/set_live_pattern.rb +70 -0
  207. data/templates/music-pro/primitives/set_tempo.rb +31 -0
  208. data/templates/music-pro/primitives/stop_voice.rb +31 -0
  209. data/templates/music-pro/primitives/support/music_protocol.rb +136 -0
  210. data/templates/music-pro/primitives/support/osc_bridge.rb +33 -0
  211. data/templates/music-pro/primitives/trigger_event.rb +44 -0
  212. data/templates/music-pro/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  213. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  214. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  215. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  216. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  217. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  218. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  219. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  220. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  221. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  222. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  223. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  224. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  225. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  226. data/templates/writer/objects/coordinator.md +14 -3
  227. data/templates/writer/objects/editor.md +1 -1
  228. data/templates/writer/objects/researcher.md +2 -2
  229. data/tools/thread-explorer.html +28 -1
  230. metadata +175 -65
  231. data/CLAUDE.md +0 -176
  232. data/Gemfile +0 -10
  233. data/Gemfile.lock +0 -233
  234. data/IMPLEMENTATION_PLAN.md +0 -1073
  235. data/Rakefile +0 -27
  236. data/design-doc-v2.md +0 -1232
  237. data/frontend/.gitignore +0 -3
  238. data/frontend/index.html +0 -17
  239. data/frontend/package-lock.json +0 -4540
  240. data/frontend/package.json +0 -36
  241. data/frontend/postcss.config.js +0 -6
  242. data/frontend/src/App.tsx +0 -103
  243. data/frontend/src/canvas/CanvasView.tsx +0 -113
  244. data/frontend/src/canvas/ForceLayout.ts +0 -115
  245. data/frontend/src/canvas/SceneManager.ts +0 -587
  246. data/frontend/src/canvas/canvasStore.ts +0 -47
  247. data/frontend/src/canvas/constants.ts +0 -95
  248. data/frontend/src/canvas/controls/CameraControls.ts +0 -98
  249. data/frontend/src/canvas/edges/MessageArc.ts +0 -149
  250. data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
  251. data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
  252. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
  253. data/frontend/src/canvas/nodes/PONode.ts +0 -249
  254. data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
  255. data/frontend/src/canvas/types.ts +0 -24
  256. data/frontend/src/components/ContextMenu.tsx +0 -68
  257. data/frontend/src/components/Inspector.tsx +0 -232
  258. data/frontend/src/components/MarkdownMessage.tsx +0 -155
  259. data/frontend/src/components/MethodList.tsx +0 -90
  260. data/frontend/src/components/ModelSelector.tsx +0 -115
  261. data/frontend/src/components/NotificationPanel.tsx +0 -130
  262. data/frontend/src/components/ObjectList.tsx +0 -78
  263. data/frontend/src/components/PaneSlot.tsx +0 -30
  264. data/frontend/src/components/SourcePane.tsx +0 -202
  265. data/frontend/src/components/SystemBar.tsx +0 -74
  266. data/frontend/src/components/Transcript.tsx +0 -76
  267. data/frontend/src/components/UsagePanel.tsx +0 -105
  268. data/frontend/src/components/Workspace.tsx +0 -260
  269. data/frontend/src/components/index.ts +0 -12
  270. data/frontend/src/hooks/useResize.ts +0 -55
  271. data/frontend/src/hooks/useWebSocket.ts +0 -517
  272. data/frontend/src/index.css +0 -103
  273. data/frontend/src/main.tsx +0 -10
  274. data/frontend/src/store/index.ts +0 -279
  275. data/frontend/src/types/index.ts +0 -165
  276. data/frontend/tailwind.config.js +0 -44
  277. data/frontend/tsconfig.json +0 -30
  278. data/frontend/vite.config.ts +0 -29
  279. data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +0 -4345
  280. data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +0 -1
  281. data/lib/prompt_objects/universal/add_primitive.rb +0 -118
  282. data/lib/prompt_objects/universal/ask_human.rb +0 -109
  283. data/lib/prompt_objects/universal/create_primitive.rb +0 -174
  284. data/lib/prompt_objects/universal/list_primitives.rb +0 -145
  285. data/lib/prompt_objects/universal/think.rb +0 -41
  286. data/prompt_objects.gemspec +0 -50
  287. data/templates/basic/.gitignore +0 -2
  288. data/templates/developer/.gitignore +0 -5
  289. data/templates/empty/.gitignore +0 -5
  290. data/templates/empty/objects/.gitkeep +0 -0
  291. data/templates/minimal/.gitignore +0 -5
  292. data/templates/writer/.gitignore +0 -5
data/exe/prompt_objects CHANGED
@@ -8,1112 +8,5 @@ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
8
8
  # the user's project Gemfile if they run this from within a Rails app, etc.
9
9
  require "prompt_objects"
10
10
  require "prompt_objects/cli"
11
- require "fileutils"
12
- require "tmpdir"
13
11
 
14
- # Sandbox manages an isolated environment for testing
15
- class Sandbox
16
- attr_reader :sandbox_dir, :objects_dir, :primitives_dir, :original_objects_dir
17
-
18
- def initialize(original_objects_dir)
19
- @original_objects_dir = File.expand_path(original_objects_dir)
20
- @sandbox_dir = Dir.mktmpdir("prompt_objects_sandbox_")
21
- @objects_dir = File.join(@sandbox_dir, "objects")
22
- @primitives_dir = File.join(@sandbox_dir, "primitives")
23
-
24
- setup
25
- end
26
-
27
- def setup
28
- FileUtils.mkdir_p(@objects_dir)
29
- FileUtils.mkdir_p(@primitives_dir)
30
-
31
- # Copy existing objects to sandbox
32
- if Dir.exist?(@original_objects_dir)
33
- Dir.glob(File.join(@original_objects_dir, "*.md")).each do |src|
34
- FileUtils.cp(src, @objects_dir)
35
- end
36
- end
37
-
38
- puts "Sandbox initialized at: #{@sandbox_dir}"
39
- puts " Objects copied: #{Dir.glob(File.join(@objects_dir, '*.md')).count}"
40
- end
41
-
42
- def cleanup(keep_changes: false)
43
- if keep_changes
44
- copy_back_changes
45
- end
46
- FileUtils.rm_rf(@sandbox_dir)
47
- puts "Sandbox cleaned up"
48
- end
49
-
50
- def copy_back_changes
51
- # Find new/modified objects
52
- new_objects = Dir.glob(File.join(@objects_dir, "*.md")).select do |sandbox_file|
53
- original = File.join(@original_objects_dir, File.basename(sandbox_file))
54
- !File.exist?(original) || File.read(sandbox_file) != File.read(original)
55
- end
56
-
57
- # Find new primitives
58
- new_primitives = Dir.glob(File.join(@primitives_dir, "*.rb"))
59
-
60
- return if new_objects.empty? && new_primitives.empty?
61
-
62
- puts "\nChanges to copy back:"
63
- new_objects.each { |f| puts " + objects/#{File.basename(f)}" }
64
- new_primitives.each { |f| puts " + primitives/#{File.basename(f)}" }
65
-
66
- print "\nCopy these to main project? (y/n): "
67
- answer = $stdin.gets&.chomp&.downcase
68
- return unless answer == "y"
69
-
70
- new_objects.each do |f|
71
- FileUtils.cp(f, @original_objects_dir)
72
- end
73
-
74
- if new_primitives.any?
75
- target_primitives = File.join(File.dirname(@original_objects_dir), "primitives")
76
- FileUtils.mkdir_p(target_primitives)
77
- new_primitives.each do |f|
78
- FileUtils.cp(f, target_primitives)
79
- end
80
- end
81
-
82
- puts "Changes copied to main project"
83
- end
84
-
85
- def show_new_files
86
- new_objects = Dir.glob(File.join(@objects_dir, "*.md")).reject do |sandbox_file|
87
- original = File.join(@original_objects_dir, File.basename(sandbox_file))
88
- File.exist?(original) && File.read(sandbox_file) == File.read(original)
89
- end
90
- new_primitives = Dir.glob(File.join(@primitives_dir, "*.rb"))
91
-
92
- { objects: new_objects, primitives: new_primitives }
93
- end
94
- end
95
-
96
- # Simple REPL for interacting with a Prompt Object
97
- class REPL
98
- def initialize(prompt_object, env, sandbox: nil)
99
- @po = prompt_object
100
- @env = env
101
- @context = env.context
102
- @show_log = true # Show message log by default
103
- @sandbox = sandbox
104
- end
105
-
106
- def run
107
- puts header
108
- if @sandbox
109
- puts "SANDBOX MODE - changes isolated from main project"
110
- puts
111
- end
112
- puts "Loaded: #{@po.name}"
113
- puts @po.description
114
- puts
115
- puts "Capabilities: #{format_capabilities}"
116
- puts "-" * 60
117
- puts "Commands: 'exit', 'history', 'log', 'log on/off', 'sandbox' (if in sandbox mode)"
118
- puts
119
-
120
- loop do
121
- print "You: "
122
- input = $stdin.gets&.chomp
123
-
124
- break if input.nil?
125
- next if input.empty?
126
-
127
- case input.downcase
128
- when "exit", "quit"
129
- handle_exit
130
- break
131
- when "history"
132
- show_history
133
- next
134
- when "log"
135
- show_message_log
136
- next
137
- when "log on"
138
- @show_log = true
139
- puts "Message log display: ON"
140
- next
141
- when "log off"
142
- @show_log = false
143
- puts "Message log display: OFF"
144
- next
145
- when "sandbox"
146
- show_sandbox_status
147
- next
148
- end
149
-
150
- # Clear log before each interaction to show only relevant messages
151
- log_start = @env.bus.log.length
152
-
153
- begin
154
- # Log the human message
155
- @env.bus.publish(from: "human", to: @po.name, message: input)
156
-
157
- @context.current_capability = @po.name
158
- response = @po.receive(input, context: @context)
159
-
160
- # Log the response back to human
161
- @env.bus.publish(from: @po.name, to: "human", message: response)
162
-
163
- # Show the message log for this interaction
164
- if @show_log
165
- puts
166
- show_interaction_log(log_start)
167
- end
168
-
169
- puts
170
- puts "#{@po.name}: #{response}"
171
- puts
172
- rescue StandardError => e
173
- puts "\nError: #{e.message}"
174
- puts e.backtrace.first(5).join("\n") if ENV["DEBUG"]
175
- puts
176
- end
177
- end
178
- end
179
-
180
- private
181
-
182
- def header
183
- <<~HEADER
184
-
185
- PromptObjects v0.1.0
186
-
187
- HEADER
188
- end
189
-
190
- def format_capabilities
191
- caps = @po.config["capabilities"] || []
192
- return "(none)" if caps.empty?
193
-
194
- caps.map do |cap_name|
195
- cap = @env.registry.get(cap_name)
196
- if cap.is_a?(PromptObjects::PromptObject)
197
- "#{cap_name} (PO)"
198
- elsif cap.is_a?(PromptObjects::Primitive)
199
- "#{cap_name}"
200
- else
201
- "#{cap_name} (?)"
202
- end
203
- end.join(", ")
204
- end
205
-
206
- def handle_exit
207
- puts "\nGoodbye!"
208
- return unless @sandbox
209
-
210
- new_files = @sandbox.show_new_files
211
- if new_files[:objects].any? || new_files[:primitives].any?
212
- puts "\nSession created files:"
213
- new_files[:objects].each { |f| puts " - #{File.basename(f)}" }
214
- new_files[:primitives].each { |f| puts " - #{File.basename(f)}" }
215
- @sandbox.cleanup(keep_changes: true)
216
- else
217
- puts "No new files created."
218
- @sandbox.cleanup(keep_changes: false)
219
- end
220
- end
221
-
222
- def show_sandbox_status
223
- unless @sandbox
224
- puts "Not in sandbox mode. Run with --sandbox to enable."
225
- return
226
- end
227
-
228
- puts "\nSandbox Status"
229
- puts " Location: #{@sandbox.sandbox_dir}"
230
- new_files = @sandbox.show_new_files
231
- if new_files[:objects].any? || new_files[:primitives].any?
232
- puts " New/modified files this session:"
233
- new_files[:objects].each { |f| puts " + #{File.basename(f)}" }
234
- new_files[:primitives].each { |f| puts " + #{File.basename(f)}" }
235
- else
236
- puts " No new files created yet."
237
- end
238
- puts
239
- end
240
-
241
- def show_history
242
- puts "\n--- Conversation History ---"
243
- @po.history.each_with_index do |msg, i|
244
- role = msg[:role].to_s.capitalize
245
- content = msg[:content]&.slice(0, 100)
246
- content += "..." if msg[:content]&.length.to_i > 100
247
- puts "#{i + 1}. [#{role}] #{content}"
248
- end
249
- puts "----------------------------\n\n"
250
- end
251
-
252
- def show_message_log
253
- puts "\n--- Full Message Log ---"
254
- puts @env.bus.format_log(50)
255
- puts "------------------------\n\n"
256
- end
257
-
258
- def show_interaction_log(start_index)
259
- entries = @env.bus.log[start_index..]
260
- return if entries.nil? || entries.empty?
261
-
262
- puts "--- Message Log ---"
263
- entries.each do |entry|
264
- time = entry[:timestamp].strftime("%H:%M:%S")
265
- from = entry[:from]
266
- to = entry[:to]
267
- msg = entry[:summary]
268
-
269
- line = "#{time} #{from} -> #{to}: #{msg}"
270
- line = line[0, 70] + "..." if line.length > 73
271
- puts line
272
- end
273
- puts "-------------------"
274
- end
275
- end
276
-
277
- # === CLI Command Handling ===
278
-
279
- def print_main_help
280
- puts <<~HELP
281
- Usage: prompt_objects <command> [options]
282
-
283
- Commands:
284
- env Manage environments (create, list, export, etc.)
285
- serve <env> Run environment as a web server
286
- message <env> <po> "text" Send a message to a PO and print the response
287
- events <env> [--session ID] Show recent events from the message bus
288
- explore [env] [--session ID] Open Thread Explorer to visualize conversations
289
- repl [name] [objects_dir] Start interactive REPL with a prompt object
290
- help Show this help message
291
-
292
- Run 'prompt_objects <command> --help' for command-specific help.
293
-
294
- Examples:
295
- prompt_objects env list # List all environments
296
- prompt_objects env create demo --template demo # Create from template
297
- prompt_objects serve my-env # Start web UI
298
- prompt_objects serve my-env --open # Start and open browser
299
- prompt_objects message my-env solver "Hello" # Send a message
300
- prompt_objects message my-env solver "Hello" --json # JSON output
301
- prompt_objects explore my-env # Visualize conversations
302
- HELP
303
- end
304
-
305
- def print_repl_help
306
- puts <<~HELP
307
- Usage: prompt_objects repl [options] [name] [objects_dir]
308
-
309
- Arguments:
310
- name Prompt object to load (default: greeter)
311
- objects_dir Directory containing .md files (default: objects)
312
-
313
- Options:
314
- --sandbox, -s Run in sandbox mode (isolates changes from main project)
315
- --help, -h Show this help message
316
-
317
- Sandbox Mode:
318
- When running with --sandbox, all new capabilities and prompt objects
319
- created during the session are stored in a temporary directory.
320
- On exit, you'll be prompted to copy changes back to the main project.
321
- HELP
322
- end
323
-
324
- def print_serve_help
325
- puts <<~HELP
326
- Usage: prompt_objects serve [options] <environment>
327
-
328
- Run an environment as a server, exposing it through different interfaces.
329
-
330
- Arguments:
331
- environment Environment name or path to environment directory
332
-
333
- Options:
334
- --web Run as web server with React UI (default)
335
- --mcp Run as MCP server (stdio transport, for Claude Desktop/Cursor)
336
- --port PORT Port for web server (default: 3000)
337
- --host HOST Host for web server (default: localhost)
338
- --open Open browser automatically
339
- --help, -h Show this help message
340
-
341
- Examples:
342
- prompt_objects serve my-assistant # Web UI (default)
343
- prompt_objects serve my-assistant --port 4000 # Custom port
344
- prompt_objects serve --mcp my-assistant # MCP server mode
345
- prompt_objects serve ./my-environment # Serve from path
346
-
347
- Claude Desktop Configuration (MCP mode):
348
- Add to claude_desktop_config.json:
349
- {
350
- "mcpServers": {
351
- "my-assistant": {
352
- "command": "prompt_objects",
353
- "args": ["serve", "--mcp", "my-assistant"]
354
- }
355
- }
356
- }
357
- HELP
358
- end
359
-
360
- def run_repl(args)
361
- options = {
362
- name: "greeter",
363
- objects_dir: "objects",
364
- sandbox: false
365
- }
366
-
367
- positional = []
368
- args.each do |arg|
369
- case arg
370
- when "--sandbox", "-s"
371
- options[:sandbox] = true
372
- when "--help", "-h"
373
- print_repl_help
374
- exit 0
375
- else
376
- positional << arg
377
- end
378
- end
379
-
380
- options[:name] = positional[0] if positional[0]
381
- options[:objects_dir] = positional[1] if positional[1]
382
-
383
- sandbox = nil
384
- primitives_dir = nil
385
-
386
- if options[:sandbox]
387
- sandbox = Sandbox.new(options[:objects_dir])
388
- objects_dir = sandbox.objects_dir
389
- primitives_dir = sandbox.primitives_dir
390
- else
391
- objects_dir = options[:objects_dir]
392
- end
393
-
394
- env = PromptObjects::Runtime.new(objects_dir: objects_dir, primitives_dir: primitives_dir)
395
-
396
- begin
397
- po = env.load_by_name(options[:name])
398
- env.load_dependencies(po)
399
- rescue PromptObjects::Error => e
400
- puts "Error loading '#{options[:name]}': #{e.message}"
401
- puts "\nAvailable objects in '#{objects_dir}/':"
402
- Dir.glob(File.join(objects_dir, "*.md")).each do |path|
403
- puts " - #{File.basename(path, '.md')}"
404
- end
405
- sandbox&.cleanup(keep_changes: false)
406
- exit 1
407
- end
408
-
409
- REPL.new(po, env, sandbox: sandbox).run
410
- end
411
-
412
- def run_serve(args)
413
- options = {
414
- web: false,
415
- mcp: false,
416
- port: 3000,
417
- host: "localhost",
418
- open: false,
419
- environment: nil
420
- }
421
-
422
- skip_next = false
423
- args.each_with_index do |arg, i|
424
- if skip_next
425
- skip_next = false
426
- next
427
- end
428
-
429
- case arg
430
- when "--web"
431
- options[:web] = true
432
- when "--mcp"
433
- options[:mcp] = true
434
- when "--port"
435
- options[:port] = args[i + 1].to_i
436
- skip_next = true
437
- when "--host"
438
- options[:host] = args[i + 1]
439
- skip_next = true
440
- when "--open", "-o"
441
- options[:open] = true
442
- when "--help", "-h"
443
- print_serve_help
444
- exit 0
445
- else
446
- # Assume it's the environment
447
- options[:environment] = arg unless arg.start_with?("-")
448
- end
449
- end
450
-
451
- unless options[:environment]
452
- puts "Error: environment name or path required"
453
- puts "Run 'prompt_objects serve --help' for usage"
454
- exit 1
455
- end
456
-
457
- # Default to web if no interface specified
458
- options[:web] = true unless options[:mcp]
459
-
460
- # Redirect stderr to a log file to avoid polluting MCP stdio
461
- if options[:mcp] && ENV["PROMPT_OBJECTS_LOG"]
462
- $stderr.reopen(ENV["PROMPT_OBJECTS_LOG"], "a")
463
- $stderr.sync = true
464
- end
465
-
466
- # Resolve environment
467
- env_path = resolve_environment(options[:environment])
468
- unless env_path
469
- $stderr.puts "Error: environment '#{options[:environment]}' not found"
470
- exit 1
471
- end
472
-
473
- # Load the runtime
474
- runtime = PromptObjects::Runtime.new(env_path: env_path)
475
-
476
- # Load all prompt objects
477
- load_all_objects(runtime, env_path)
478
-
479
- if options[:mcp]
480
- connector = PromptObjects::Connectors::MCP.new(runtime: runtime)
481
- connector.start
482
- elsif options[:web]
483
- require "prompt_objects/server"
484
-
485
- # Rebuild frontend to ensure built assets match source
486
- rebuild_frontend
487
-
488
- # Open browser if requested
489
- if options[:open]
490
- url = "http://#{options[:host]}:#{options[:port]}"
491
- open_browser(url)
492
- end
493
-
494
- PromptObjects::Server.start(
495
- runtime: runtime,
496
- host: options[:host],
497
- port: options[:port],
498
- env_path: env_path
499
- )
500
- end
501
- end
502
-
503
- def rebuild_frontend
504
- # Find the frontend directory relative to this executable
505
- gem_root = File.expand_path("../..", __FILE__)
506
- frontend_dir = File.join(gem_root, "frontend")
507
-
508
- unless Dir.exist?(frontend_dir)
509
- # No frontend source (e.g., installed as gem without source) — skip
510
- return
511
- end
512
-
513
- node_modules = File.join(frontend_dir, "node_modules")
514
- unless Dir.exist?(node_modules)
515
- puts "Frontend dependencies not installed. Run: cd frontend && npm install"
516
- return
517
- end
518
-
519
- print "Building frontend... "
520
- result = system("npm run build --prefix #{frontend_dir}", out: File::NULL, err: File::NULL)
521
- if result
522
- puts "done"
523
- else
524
- puts "failed (serving stale assets)"
525
- end
526
- end
527
-
528
- def open_browser(url)
529
- case RUBY_PLATFORM
530
- when /darwin/
531
- system("open", url)
532
- when /linux/
533
- system("xdg-open", url)
534
- when /mswin|mingw/
535
- system("start", url)
536
- end
537
- end
538
-
539
- def resolve_environment(name_or_path)
540
- # Check if it's already a path
541
- if File.directory?(name_or_path)
542
- manifest_path = File.join(name_or_path, "manifest.yml")
543
- return name_or_path if File.exist?(manifest_path)
544
- end
545
-
546
- # Check in standard locations
547
- locations = [
548
- File.join(Dir.home, ".prompt_objects", "environments", name_or_path),
549
- File.join(".", name_or_path)
550
- ]
551
-
552
- locations.each do |path|
553
- manifest_path = File.join(path, "manifest.yml")
554
- return path if File.exist?(manifest_path)
555
- end
556
-
557
- nil
558
- end
559
-
560
- def load_all_objects(runtime, env_path)
561
- objects_dir = File.join(env_path, "objects")
562
- return unless Dir.exist?(objects_dir)
563
-
564
- Dir.glob(File.join(objects_dir, "*.md")).each do |path|
565
- runtime.load_prompt_object(path)
566
- rescue StandardError => e
567
- $stderr.puts "Warning: Failed to load #{path}: #{e.message}"
568
- end
569
-
570
- runtime.registry.prompt_objects.each do |po|
571
- runtime.load_dependencies(po)
572
- end
573
- end
574
-
575
- def print_message_help
576
- puts <<~HELP
577
- Usage: prompt_objects message [options] <environment> <po_name> "message"
578
-
579
- Send a message to a prompt object and print the response.
580
-
581
- Arguments:
582
- environment Environment name or path
583
- po_name Name of the prompt object to message
584
- message The message to send (quoted string)
585
-
586
- Options:
587
- --json Output response as JSON
588
- --events Also print the event log for this interaction
589
- --new-thread Start a new thread for this message
590
- --session ID Use a specific session ID
591
- --help, -h Show this help
592
-
593
- If a server is running for the environment, the message is sent via
594
- HTTP to the running server (and streams to the web UI in real time).
595
- Otherwise, a standalone runtime is started for the request.
596
-
597
- Examples:
598
- prompt_objects message my-env solver "What patterns do you see?"
599
- prompt_objects message my-env solver "Solve task 1" --json
600
- prompt_objects message my-env solver "Try again" --new-thread
601
- HELP
602
- end
603
-
604
- def run_message(args)
605
- options = {
606
- json: false,
607
- events: false,
608
- new_thread: false,
609
- session_id: nil
610
- }
611
-
612
- positional = []
613
- skip_next = false
614
-
615
- args.each_with_index do |arg, i|
616
- if skip_next
617
- skip_next = false
618
- next
619
- end
620
-
621
- case arg
622
- when "--json"
623
- options[:json] = true
624
- when "--events"
625
- options[:events] = true
626
- when "--new-thread"
627
- options[:new_thread] = true
628
- when "--session"
629
- options[:session_id] = args[i + 1]
630
- skip_next = true
631
- when "--help", "-h"
632
- print_message_help
633
- exit 0
634
- else
635
- positional << arg
636
- end
637
- end
638
-
639
- env_name = positional[0]
640
- po_name = positional[1]
641
- message_text = positional[2]
642
-
643
- unless env_name && po_name && message_text
644
- puts "Error: environment, po_name, and message are required"
645
- puts "Run 'prompt_objects message --help' for usage"
646
- exit 1
647
- end
648
-
649
- # Resolve environment
650
- env_path = resolve_environment(env_name)
651
- unless env_path
652
- $stderr.puts "Error: environment '#{env_name}' not found"
653
- exit 1
654
- end
655
-
656
- # Check for running server
657
- require "prompt_objects/server"
658
- server_info = PromptObjects::Server.read_server_file(env_path)
659
-
660
- if server_info
661
- # Send via HTTP to running server
662
- send_message_via_http(server_info, po_name, message_text, options)
663
- else
664
- # Standalone mode
665
- send_message_standalone(env_path, po_name, message_text, options)
666
- end
667
- end
668
-
669
- def send_message_via_http(server_info, po_name, message_text, options)
670
- require "net/http"
671
- require "uri"
672
-
673
- uri = URI("http://#{server_info[:host]}:#{server_info[:port]}/api/prompt_objects/#{po_name}/message")
674
-
675
- body = { message: message_text }
676
- body[:new_thread] = true if options[:new_thread]
677
- body[:session_id] = options[:session_id] if options[:session_id]
678
-
679
- begin
680
- response = Net::HTTP.post(uri, body.to_json, "Content-Type" => "application/json")
681
- data = JSON.parse(response.body)
682
-
683
- if data["error"]
684
- $stderr.puts "Error: #{data['error']}"
685
- exit 1
686
- end
687
-
688
- if options[:json]
689
- puts JSON.pretty_generate(data)
690
- else
691
- puts data["response"]
692
- end
693
-
694
- if options[:events] && data["session_id"]
695
- events_uri = URI("http://#{server_info[:host]}:#{server_info[:port]}/api/events/session/#{data['session_id']}")
696
- events_response = Net::HTTP.get(events_uri)
697
- events_data = JSON.parse(events_response)
698
-
699
- if events_data["events"]&.any?
700
- $stderr.puts
701
- $stderr.puts "--- Events ---"
702
- events_data["events"].each do |e|
703
- $stderr.puts "#{e['timestamp']} #{e['from']} -> #{e['to']}: #{e['summary']}"
704
- end
705
- $stderr.puts "--------------"
706
- end
707
- end
708
- rescue Errno::ECONNREFUSED
709
- $stderr.puts "Error: Server at #{server_info[:host]}:#{server_info[:port]} is not responding"
710
- $stderr.puts "The .server file may be stale. Try running standalone."
711
- exit 1
712
- end
713
- end
714
-
715
- def send_message_standalone(env_path, po_name, message_text, options)
716
- runtime = PromptObjects::Runtime.new(env_path: env_path)
717
- load_all_objects(runtime, env_path)
718
-
719
- po = runtime.registry.get(po_name)
720
- unless po.is_a?(PromptObjects::PromptObject)
721
- $stderr.puts "Error: prompt object '#{po_name}' not found"
722
- $stderr.puts "Available: #{runtime.loaded_objects.join(', ')}"
723
- exit 1
724
- end
725
-
726
- # Create new thread if requested
727
- po.new_thread if options[:new_thread]
728
-
729
- # Switch session if specified
730
- if options[:session_id]
731
- po.switch_session(options[:session_id])
732
- end
733
-
734
- session_id = po.session_id
735
- log_start = runtime.bus.log.length
736
-
737
- context = runtime.context
738
- context.current_capability = "human"
739
-
740
- # Log to bus
741
- runtime.bus.publish(from: "human", to: po.name, message: message_text, session_id: session_id)
742
-
743
- response = po.receive(message_text, context: context)
744
-
745
- # Log response to bus
746
- runtime.bus.publish(from: po.name, to: "human", message: response, session_id: session_id)
747
-
748
- if options[:json]
749
- data = {
750
- response: response,
751
- po_name: po.name,
752
- session_id: session_id,
753
- event_count: runtime.bus.log.length - log_start
754
- }
755
- puts JSON.pretty_generate(data)
756
- else
757
- puts response
758
- end
759
-
760
- if options[:events]
761
- entries = runtime.bus.log[log_start..]
762
- if entries&.any?
763
- $stderr.puts
764
- $stderr.puts "--- Events ---"
765
- entries.each do |entry|
766
- time = entry[:timestamp].strftime("%H:%M:%S")
767
- $stderr.puts "#{time} #{entry[:from]} -> #{entry[:to]}: #{entry[:summary]}"
768
- end
769
- $stderr.puts "--------------"
770
- end
771
- end
772
- end
773
-
774
- def print_events_help
775
- puts <<~HELP
776
- Usage: prompt_objects events [options] <environment>
777
-
778
- Show recent events from the message bus.
779
-
780
- Arguments:
781
- environment Environment name or path
782
-
783
- Options:
784
- --session ID Show events for a specific session
785
- --count N Number of events to show (default: 50)
786
- --json Output as JSON
787
- --help, -h Show this help
788
-
789
- Examples:
790
- prompt_objects events my-env
791
- prompt_objects events my-env --session abc-123
792
- prompt_objects events my-env --count 100 --json
793
- HELP
794
- end
795
-
796
- def run_events(args)
797
- options = { count: 50, session_id: nil, json: false }
798
- positional = []
799
- skip_next = false
800
-
801
- args.each_with_index do |arg, i|
802
- if skip_next
803
- skip_next = false
804
- next
805
- end
806
-
807
- case arg
808
- when "--session"
809
- options[:session_id] = args[i + 1]
810
- skip_next = true
811
- when "--count"
812
- options[:count] = args[i + 1].to_i
813
- skip_next = true
814
- when "--json"
815
- options[:json] = true
816
- when "--help", "-h"
817
- print_events_help
818
- exit 0
819
- else
820
- positional << arg
821
- end
822
- end
823
-
824
- env_name = positional[0]
825
- unless env_name
826
- puts "Error: environment name required"
827
- puts "Run 'prompt_objects events --help' for usage"
828
- exit 1
829
- end
830
-
831
- env_path = resolve_environment(env_name)
832
- unless env_path
833
- $stderr.puts "Error: environment '#{env_name}' not found"
834
- exit 1
835
- end
836
-
837
- # Check for running server
838
- require "prompt_objects/server"
839
- server_info = PromptObjects::Server.read_server_file(env_path)
840
-
841
- if server_info
842
- show_events_via_http(server_info, options)
843
- else
844
- show_events_standalone(env_path, options)
845
- end
846
- end
847
-
848
- def show_events_via_http(server_info, options)
849
- require "net/http"
850
- require "uri"
851
-
852
- if options[:session_id]
853
- uri = URI("http://#{server_info[:host]}:#{server_info[:port]}/api/events/session/#{options[:session_id]}")
854
- else
855
- uri = URI("http://#{server_info[:host]}:#{server_info[:port]}/api/events?count=#{options[:count]}")
856
- end
857
-
858
- response = Net::HTTP.get(uri)
859
- data = JSON.parse(response)
860
-
861
- if options[:json]
862
- puts JSON.pretty_generate(data)
863
- else
864
- print_events(data["events"] || [])
865
- end
866
- rescue Errno::ECONNREFUSED
867
- $stderr.puts "Error: Server not responding"
868
- exit 1
869
- end
870
-
871
- def show_events_standalone(env_path, options)
872
- db_path = File.join(env_path, "sessions.db")
873
- unless File.exist?(db_path)
874
- puts "No event history (sessions.db not found)"
875
- exit 0
876
- end
877
-
878
- store = PromptObjects::Session::Store.new(db_path)
879
-
880
- events = if options[:session_id]
881
- store.get_events(session_id: options[:session_id])
882
- else
883
- store.get_recent_events(options[:count])
884
- end
885
-
886
- if options[:json]
887
- formatted = events.map do |e|
888
- {
889
- id: e[:id],
890
- from: e[:from],
891
- to: e[:to],
892
- summary: e[:summary],
893
- message: e[:message],
894
- timestamp: e[:timestamp]&.iso8601,
895
- session_id: e[:session_id]
896
- }
897
- end
898
- puts JSON.pretty_generate(formatted)
899
- else
900
- print_events(events)
901
- end
902
-
903
- store.close
904
- end
905
-
906
- def print_events(events)
907
- if events.empty?
908
- puts "No events found."
909
- return
910
- end
911
-
912
- events.each do |e|
913
- ts = e[:timestamp] || e["timestamp"]
914
- ts = ts.is_a?(Time) ? ts.strftime("%H:%M:%S") : ts&.split("T")&.last&.split(".")&.first
915
- from = e[:from] || e["from"]
916
- to = e[:to] || e["to"]
917
- summary = e[:summary] || e["summary"]
918
-
919
- line = "#{ts} #{from} -> #{to}: #{summary}"
920
- line = line[0, 120] + "..." if line.length > 123
921
- puts line
922
- end
923
- end
924
-
925
- # === Explore Command ===
926
-
927
- def run_explore(args)
928
- options = { session_id: nil }
929
- positional = []
930
- skip_next = false
931
-
932
- args.each_with_index do |arg, i|
933
- if skip_next
934
- skip_next = false
935
- next
936
- end
937
-
938
- case arg
939
- when "--session"
940
- options[:session_id] = args[i + 1]
941
- skip_next = true
942
- when "--help", "-h"
943
- print_explore_help
944
- exit 0
945
- else
946
- positional << arg
947
- end
948
- end
949
-
950
- explorer_html = File.expand_path("../tools/thread-explorer.html", __dir__)
951
-
952
- # No args: just open the empty visualizer
953
- unless positional[0]
954
- open_in_browser("file://#{explorer_html}")
955
- puts "Opened Thread Explorer in browser"
956
- exit 0
957
- end
958
-
959
- env_path = resolve_environment(positional[0])
960
- unless env_path
961
- $stderr.puts "Error: environment '#{positional[0]}' not found"
962
- exit 1
963
- end
964
-
965
- db_path = File.join(env_path, "sessions.db")
966
- unless File.exist?(db_path)
967
- $stderr.puts "No session data found (sessions.db not found)"
968
- $stderr.puts "Opening empty Thread Explorer..."
969
- open_in_browser("file://#{explorer_html}")
970
- exit 0
971
- end
972
-
973
- store = PromptObjects::Session::Store.new(db_path)
974
-
975
- if options[:session_id]
976
- # Export a specific thread
977
- export_and_open(store, options[:session_id], explorer_html)
978
- else
979
- # List sessions and let user choose, or export the most recent root thread
980
- # Query root sessions directly (list_sessions requires po_name)
981
- rows = store.instance_variable_get(:@db).execute(<<~SQL)
982
- SELECT * FROM sessions
983
- WHERE thread_type IS NULL OR thread_type = 'root'
984
- ORDER BY created_at ASC
985
- SQL
986
- root_sessions = rows.map { |r| { id: r["id"], po_name: r["po_name"], name: r["name"], thread_type: r["thread_type"], created_at: r["created_at"] } }
987
-
988
- if root_sessions.empty?
989
- $stderr.puts "No root sessions found"
990
- open_in_browser("file://#{explorer_html}")
991
- exit 0
992
- end
993
-
994
- # Show available sessions
995
- puts "Root threads:"
996
- root_sessions.last(10).each_with_index do |s, i|
997
- name = s[:name] || "(unnamed)"
998
- po = s[:po_name] || "?"
999
- time = s[:created_at]
1000
- time_str = time.is_a?(Time) ? time.strftime("%Y-%m-%d %H:%M") : time.to_s[0, 16]
1001
- puts " #{i + 1}. [#{po}] #{name} (#{time_str}) #{s[:id]}"
1002
- end
1003
-
1004
- puts
1005
- puts "Opening most recent thread..."
1006
- export_and_open(store, root_sessions.last[:id], explorer_html)
1007
- end
1008
-
1009
- store.close
1010
- end
1011
-
1012
- def export_and_open(store, session_id, explorer_html)
1013
- data = store.export_thread_tree_json(session_id)
1014
- unless data
1015
- $stderr.puts "Error: session '#{session_id}' not found"
1016
- exit 1
1017
- end
1018
-
1019
- json_str = JSON.generate(data)
1020
-
1021
- # Read the explorer HTML and inject the data as auto-load
1022
- html_content = File.read(explorer_html)
1023
- inject_script = <<~JS
1024
- <script>
1025
- // Auto-load embedded data
1026
- window.addEventListener('DOMContentLoaded', function() {
1027
- var data = #{json_str};
1028
- if (typeof loadData === 'function') loadData(data);
1029
- });
1030
- </script>
1031
- JS
1032
-
1033
- # Insert before </body>
1034
- html_content = html_content.sub("</body>", "#{inject_script}</body>")
1035
-
1036
- # Write to temp file
1037
- require "tempfile"
1038
- tmp = Tempfile.new(["thread-explorer-", ".html"])
1039
- tmp.write(html_content)
1040
- tmp.close
1041
-
1042
- open_in_browser("file://#{tmp.path}")
1043
- puts "Opened Thread Explorer for session #{session_id}"
1044
-
1045
- # Keep temp file around briefly so the browser can load it
1046
- sleep 2
1047
- end
1048
-
1049
- def open_in_browser(url)
1050
- case RUBY_PLATFORM
1051
- when /darwin/
1052
- system("open", url)
1053
- when /linux/
1054
- system("xdg-open", url)
1055
- when /mswin|mingw|cygwin/
1056
- system("start", url)
1057
- else
1058
- $stderr.puts "Open this URL in your browser: #{url}"
1059
- end
1060
- end
1061
-
1062
- def print_explore_help
1063
- puts <<~HELP
1064
- Usage: prompt_objects explore [env] [options]
1065
-
1066
- Open the Thread Explorer to visualize conversation threads.
1067
-
1068
- Arguments:
1069
- env Environment name or path (optional)
1070
-
1071
- Options:
1072
- --session ID Open a specific thread by session ID
1073
- --help, -h Show this help message
1074
-
1075
- Examples:
1076
- prompt_objects explore # Open empty explorer
1077
- prompt_objects explore my-env # Open most recent thread
1078
- prompt_objects explore my-env --session abc123 # Open specific thread
1079
- HELP
1080
- end
1081
-
1082
- # === Main Entry Point ===
1083
-
1084
- def run_env(args)
1085
- cmd = PromptObjects::CLI::EnvCommand.new
1086
- cmd.run(args)
1087
- end
1088
-
1089
- def main
1090
- if ARGV.empty?
1091
- print_main_help
1092
- exit 0
1093
- end
1094
-
1095
- command = ARGV[0]
1096
- args = ARGV[1..]
1097
-
1098
- case command
1099
- when "env"
1100
- run_env(args)
1101
- when "repl"
1102
- run_repl(args)
1103
- when "serve"
1104
- run_serve(args)
1105
- when "message", "msg"
1106
- run_message(args)
1107
- when "events"
1108
- run_events(args)
1109
- when "explore"
1110
- run_explore(args)
1111
- when "help", "--help", "-h"
1112
- print_main_help
1113
- else
1114
- # Legacy behavior: treat as repl with name argument
1115
- run_repl(ARGV)
1116
- end
1117
- end
1118
-
1119
- main
12
+ PromptObjects::CLI.run(ARGV)