prompt_objects 0.6.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 (291) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -0
  3. data/README.md +93 -22
  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 +1337 -20
  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 +171 -519
  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 +549 -53
  65. data/lib/prompt_objects/server/app.rb +133 -14
  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 -227
  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 +600 -13
  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 +23 -15
  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 +8 -14
  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 +10 -14
  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/{store_env_data.rb → set_env_data.rb} +34 -20
  87. data/lib/prompt_objects/version.rb +5 -0
  88. data/lib/prompt_objects.rb +33 -8
  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 +2 -2
  92. data/templates/arc-agi-1/objects/solver.md +6 -5
  93. data/templates/arc-agi-1/objects/verifier.md +2 -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 +1 -1
  230. metadata +172 -65
  231. data/CLAUDE.md +0 -177
  232. data/Gemfile +0 -10
  233. data/Rakefile +0 -27
  234. data/frontend/.gitignore +0 -3
  235. data/frontend/index.html +0 -17
  236. data/frontend/package-lock.json +0 -4540
  237. data/frontend/package.json +0 -36
  238. data/frontend/postcss.config.js +0 -6
  239. data/frontend/src/App.tsx +0 -104
  240. data/frontend/src/canvas/CanvasView.tsx +0 -113
  241. data/frontend/src/canvas/ForceLayout.ts +0 -115
  242. data/frontend/src/canvas/SceneManager.ts +0 -587
  243. data/frontend/src/canvas/canvasStore.ts +0 -47
  244. data/frontend/src/canvas/constants.ts +0 -95
  245. data/frontend/src/canvas/controls/CameraControls.ts +0 -98
  246. data/frontend/src/canvas/edges/MessageArc.ts +0 -149
  247. data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
  248. data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
  249. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
  250. data/frontend/src/canvas/nodes/PONode.ts +0 -249
  251. data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
  252. data/frontend/src/canvas/types.ts +0 -24
  253. data/frontend/src/components/ContextMenu.tsx +0 -68
  254. data/frontend/src/components/EnvDataPane.tsx +0 -69
  255. data/frontend/src/components/Inspector.tsx +0 -263
  256. data/frontend/src/components/MarkdownMessage.tsx +0 -155
  257. data/frontend/src/components/MethodList.tsx +0 -90
  258. data/frontend/src/components/ModelSelector.tsx +0 -115
  259. data/frontend/src/components/NotificationPanel.tsx +0 -130
  260. data/frontend/src/components/ObjectList.tsx +0 -78
  261. data/frontend/src/components/PaneSlot.tsx +0 -30
  262. data/frontend/src/components/SourcePane.tsx +0 -202
  263. data/frontend/src/components/SystemBar.tsx +0 -74
  264. data/frontend/src/components/Transcript.tsx +0 -76
  265. data/frontend/src/components/UsagePanel.tsx +0 -105
  266. data/frontend/src/components/Workspace.tsx +0 -260
  267. data/frontend/src/components/index.ts +0 -12
  268. data/frontend/src/hooks/useResize.ts +0 -55
  269. data/frontend/src/hooks/useWebSocket.ts +0 -559
  270. data/frontend/src/index.css +0 -102
  271. data/frontend/src/main.tsx +0 -10
  272. data/frontend/src/store/index.ts +0 -311
  273. data/frontend/src/types/index.ts +0 -176
  274. data/frontend/tailwind.config.js +0 -44
  275. data/frontend/tsconfig.json +0 -30
  276. data/frontend/vite.config.ts +0 -29
  277. data/lib/prompt_objects/server/public/assets/index-DEPawnfZ.js +0 -4345
  278. data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +0 -1
  279. data/lib/prompt_objects/universal/add_primitive.rb +0 -118
  280. data/lib/prompt_objects/universal/ask_human.rb +0 -109
  281. data/lib/prompt_objects/universal/create_primitive.rb +0 -174
  282. data/lib/prompt_objects/universal/list_primitives.rb +0 -145
  283. data/lib/prompt_objects/universal/think.rb +0 -41
  284. data/lib/prompt_objects/universal/update_env_data.rb +0 -88
  285. data/prompt_objects.gemspec +0 -50
  286. data/templates/basic/.gitignore +0 -2
  287. data/templates/developer/.gitignore +0 -5
  288. data/templates/empty/.gitignore +0 -5
  289. data/templates/empty/objects/.gitkeep +0 -0
  290. data/templates/minimal/.gitignore +0 -5
  291. data/templates/writer/.gitignore +0 -5
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require_relative "support/osc_bridge"
5
+
6
+ module PromptObjects
7
+ module Primitives
8
+ class QuerySonicPi < Primitive
9
+ RESPONSE_PORT = 4561
10
+ TIMEOUT = 2 # seconds to wait for response
11
+
12
+ def name
13
+ "query_sonic_pi"
14
+ end
15
+
16
+ def description
17
+ "Query Sonic Pi for its current live state — which voices are active, their synths, patterns, and BPM. " \
18
+ "Returns the actual state from Sonic Pi, not what we think we sent. " \
19
+ "Returns an error if Sonic Pi is not running or the boot script isn't loaded."
20
+ end
21
+
22
+ def parameters
23
+ {
24
+ type: "object",
25
+ properties: {},
26
+ required: []
27
+ }
28
+ end
29
+
30
+ def receive(message, context:)
31
+ # Open a UDP socket to receive the response
32
+ socket = UDPSocket.new
33
+ socket.bind("127.0.0.1", RESPONSE_PORT)
34
+
35
+ # Send the query to Sonic Pi, telling it which port to respond on
36
+ OscBridge.send_message("/prompt_objects/query", RESPONSE_PORT)
37
+
38
+ # Wait for response with timeout
39
+ ready = IO.select([socket], nil, nil, TIMEOUT)
40
+
41
+ unless ready
42
+ socket.close
43
+ return "Sonic Pi is not responding. Make sure:\n" \
44
+ "1. Sonic Pi is open and running\n" \
45
+ "2. The boot script from extras/sonic_pi_boot.rb is loaded and running\n" \
46
+ "3. The script includes the /prompt_objects/query handler"
47
+ end
48
+
49
+ data, = socket.recvfrom(4096)
50
+ socket.close
51
+
52
+ # Parse OSC response — extract the string argument
53
+ state_str = parse_osc_string(data)
54
+ return "Received empty response from Sonic Pi" unless state_str && !state_str.empty?
55
+
56
+ # Parse the state string into structured data
57
+ parse_state(state_str)
58
+
59
+ rescue Errno::EADDRINUSE
60
+ "Port #{RESPONSE_PORT} is already in use — another query may be in progress. Try again in a moment."
61
+ rescue => e
62
+ socket&.close
63
+ "Error querying Sonic Pi: #{e.message}"
64
+ end
65
+
66
+ private
67
+
68
+ # Parse the raw OSC packet to extract the string argument.
69
+ # OSC format: address (null-padded to 4-byte boundary), type tag string, arguments
70
+ def parse_osc_string(data)
71
+ return nil if data.nil? || data.empty?
72
+
73
+ # Find the address string (starts at byte 0, null-terminated)
74
+ addr_end = data.index("\x00")
75
+ return nil unless addr_end
76
+
77
+ # Skip address + padding to 4-byte boundary
78
+ pos = addr_end + 1
79
+ pos += 1 while pos % 4 != 0
80
+
81
+ # Type tag string starts with ','
82
+ return nil unless data[pos] == ","
83
+ type_tag_end = data.index("\x00", pos)
84
+ return nil unless type_tag_end
85
+
86
+ type_tags = data[(pos + 1)...type_tag_end]
87
+
88
+ # Skip type tag + padding
89
+ pos = type_tag_end + 1
90
+ pos += 1 while pos % 4 != 0
91
+
92
+ # First argument should be a string (type 's')
93
+ if type_tags[0] == "s"
94
+ str_end = data.index("\x00", pos)
95
+ return nil unless str_end
96
+ data[pos...str_end]
97
+ else
98
+ nil
99
+ end
100
+ end
101
+
102
+ def parse_state(state_str)
103
+ # Format: "bpm|slots_used|slots_free||voice|active|synth|notes|durs||..."
104
+ parts = state_str.split("||")
105
+ return "Unexpected state format: #{state_str}" if parts.empty?
106
+
107
+ # Header: bpm|slots_used|slots_free
108
+ header = parts[0].split("|")
109
+ bpm = header[0].to_i
110
+ slots_used = header[1].to_i
111
+ slots_free = header[2].to_i
112
+
113
+ voices = {}
114
+ parts[1..].each do |part|
115
+ fields = part.split("|")
116
+ next if fields.length < 3
117
+
118
+ voice_name = fields[0]
119
+ voices[voice_name] = {
120
+ "active" => fields[1] == "true",
121
+ "synth" => fields[2],
122
+ "notes" => fields[3] && !fields[3].empty? ? fields[3] : nil,
123
+ "durations" => fields[4] && !fields[4].empty? ? fields[4] : nil
124
+ }
125
+ end
126
+
127
+ state = {
128
+ "connected" => true,
129
+ "bpm" => bpm,
130
+ "slots_used" => slots_used,
131
+ "slots_free" => slots_free,
132
+ "voices" => voices
133
+ }
134
+
135
+ JSON.pretty_generate(state)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/osc_bridge"
4
+
5
+
6
+ module PromptObjects
7
+ module Primitives
8
+ class SetSynth < Primitive
9
+ # Pre-tuned sound-pack presets installed by the boot script. Each picks a
10
+ # warm synth and shapes the envelope, filter, and level so notes never
11
+ # click or sound harsh. Prefer these over raw synth names.
12
+ PRESETS = %w[warm_lead soft_pad round_bass warm_bell soft_kick soft_hat].freeze
13
+
14
+ def name
15
+ "set_synth"
16
+ end
17
+
18
+ def description
19
+ "Set a voice's sound to one of the warm sound-pack presets: #{PRESETS.join(', ')}. " \
20
+ "Use warm_lead for melodies, soft_pad for sustained chords, round_bass for bass, " \
21
+ "warm_bell for bells/plucks, soft_kick for kicks, soft_hat for hi-hats. " \
22
+ "The engine handles envelopes, filtering, and reverb — you just pick the preset."
23
+ end
24
+
25
+ def parameters
26
+ {
27
+ type: "object",
28
+ properties: {
29
+ voice: {
30
+ type: "string",
31
+ description: "Which voice to change (e.g. lead, bass, perc, pad, or any voice name)"
32
+ },
33
+ synth: {
34
+ type: "string",
35
+ description: "Sound-pack preset: #{PRESETS.join(', ')}. (An advanced raw Sonic Pi synth name also works but won't be as warm.)"
36
+ }
37
+ },
38
+ required: %w[voice synth]
39
+ }
40
+ end
41
+
42
+ def receive(message, context:)
43
+ voice = message[:voice] || message["voice"]
44
+ synth = (message[:synth] || message["synth"]).to_s.strip
45
+
46
+ OscBridge.send_message("/chiptune/synth", voice, synth)
47
+ "Set #{voice} sound to #{synth}"
48
+ rescue => e
49
+ "Error setting synth: #{e.message}. Is Sonic Pi running with the boot script loaded?"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/osc_bridge"
4
+
5
+
6
+ module PromptObjects
7
+ module Primitives
8
+ class SetTempo < Primitive
9
+ def name
10
+ "set_tempo"
11
+ end
12
+
13
+ def description
14
+ "Set the global tempo (BPM) for the whole ensemble. There is ONE shared tempo — " \
15
+ "every voice locks to it via the master clock, so all voices stay in time. " \
16
+ "Conductor-only: voices should not change the tempo. For background music, 70-90 BPM."
17
+ end
18
+
19
+ def parameters
20
+ {
21
+ type: "object",
22
+ properties: {
23
+ bpm: {
24
+ type: "integer",
25
+ description: "Tempo in beats per minute (e.g. 84). Typical range 60-160."
26
+ }
27
+ },
28
+ required: %w[bpm]
29
+ }
30
+ end
31
+
32
+ def receive(message, context:)
33
+ bpm = (message[:bpm] || message["bpm"]).to_i
34
+ return "Error: bpm must be a positive integer" unless bpm > 0
35
+
36
+ OscBridge.send_message("/chiptune/tempo", bpm)
37
+ "Set the shared tempo to #{bpm} BPM. All voices will lock to it on the next bar."
38
+ rescue => e
39
+ "Error setting tempo: #{e.message}. Is Sonic Pi running with the boot script loaded?"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/osc_bridge"
4
+
5
+
6
+ module PromptObjects
7
+ module Primitives
8
+ class StopVoice < Primitive
9
+ def name
10
+ "stop_voice"
11
+ end
12
+
13
+ def description
14
+ "Stop a voice's live loop in Sonic Pi"
15
+ end
16
+
17
+ def parameters
18
+ {
19
+ type: "object",
20
+ properties: {
21
+ voice: {
22
+ type: "string",
23
+ description: "Which voice to stop (e.g. lead, bass, perc, pad, or any voice name), or 'all' to stop everything"
24
+ }
25
+ },
26
+ required: %w[voice]
27
+ }
28
+ end
29
+
30
+ def receive(message, context:)
31
+ voice = message[:voice] || message["voice"]
32
+
33
+ OscBridge.send_message("/chiptune/stop", voice)
34
+ if voice == "all"
35
+ "Stopped all voices"
36
+ else
37
+ "Stopped #{voice} voice"
38
+ end
39
+ rescue => e
40
+ "Error stopping voice: #{e.message}. Is Sonic Pi running with the boot script loaded?"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "osc-ruby"
4
+
5
+ module PromptObjects
6
+ module Primitives
7
+ # Shared OSC client for communicating with Sonic Pi.
8
+ # Thread-safe via Mutex. Used by music primitives and the heartbeat service.
9
+ module OscBridge
10
+ SONIC_PI_HOST = ENV.fetch("SONIC_PI_HOST", "127.0.0.1")
11
+ SONIC_PI_PORT = ENV.fetch("SONIC_PI_PORT", "4560").to_i
12
+
13
+ @client = nil
14
+ @mutex = Mutex.new
15
+
16
+ def self.client
17
+ @mutex.synchronize do
18
+ @client ||= OSC::Client.new(SONIC_PI_HOST, SONIC_PI_PORT)
19
+ end
20
+ end
21
+
22
+ def self.send_message(path, *args)
23
+ client.send(OSC::Message.new(path, *args))
24
+ end
25
+
26
+ def self.connected?
27
+ !@client.nil?
28
+ rescue
29
+ false
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Colin Harris
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,58 @@
1
+ module OSC
2
+ class AddressPattern
3
+ def initialize(pattern)
4
+ @pattern = pattern
5
+
6
+ generate_regex_from_pattern
7
+ end
8
+
9
+ def match?(address)
10
+ !!(@re.nil? || @re.match(address))
11
+ end
12
+
13
+ private
14
+
15
+ def generate_regex_from_pattern
16
+ case @pattern
17
+ when nil
18
+ @re = @pattern
19
+ when Regexp
20
+ @re = @pattern
21
+ when String
22
+ # i'm unsure what this does
23
+ # @pattern.gsub!(/[.^(|)]/, '\\1')
24
+
25
+ # handles osc single char wildcard matching
26
+ @pattern.gsub!(/\?/, '[^/]')
27
+
28
+ # handles ** - used in creating osc routers
29
+ # TODO: turns out the OSC 1.1 spec says that we shoudl use "//"
30
+ # this will be implemented in the next major release of this gem
31
+ #@pattern.gsub!(/\*\*/, '.*' )
32
+
33
+ # handles osc * - 0 or more matching
34
+ @pattern.gsub!(/\*[^\*]/, '[^/]*')
35
+
36
+ # handles [!] matching
37
+ @pattern.gsub!(/\[!/, '[^')
38
+
39
+ # handles {} matching
40
+ @pattern.gsub!(/\{/, '(')
41
+ @pattern.gsub!(/,/, '|')
42
+ @pattern.gsub!(/\}/, ')')
43
+
44
+
45
+ # keeps from matching before the begining of the pattern
46
+ @pattern.gsub!(/\A/, '\A')
47
+
48
+ # keeps from matching beyond the end,
49
+ # eg. pattern /hi does not match /hidden
50
+ @pattern.gsub!(/\z/, '\z')
51
+
52
+ @re = Regexp.new(@pattern)
53
+ else
54
+ raise(ArgumentError, 'invalid pattern')
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ require 'ipaddr'
2
+
3
+ module OSC
4
+ class BroadcastClient
5
+
6
+ BROADCAST_ADDRESS = '<broadcast>'
7
+
8
+ attr_reader :port
9
+
10
+ def initialize(port, local_ip = nil)
11
+ @port = port
12
+ @so = UDPSocket.new
13
+ @so.setsockopt Socket::SOL_SOCKET, Socket::SO_BROADCAST, true
14
+ if local_ip
15
+ @so.setsockopt Socket::IPPROTO_IP, Socket::IP_MULTICAST_IF, IPAddr.new(local_ip).hton
16
+ @so.bind(local_ip, 0)
17
+ end
18
+ end
19
+
20
+ def send(mesg)
21
+ @so.send(mesg.encode, 0, BROADCAST_ADDRESS, @port)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,46 @@
1
+ module OSC
2
+ class Bundle
3
+ attr_accessor :timetag
4
+
5
+ def initialize(timetag=nil, *args)
6
+ @timetag = timetag
7
+ @args = args
8
+ end
9
+
10
+ def encode()
11
+ s = OSCString.new('#bundle').encode
12
+ s << encode_timetag(@timetag)
13
+ s << @args.collect do |x|
14
+ x2 = x.encode; [x2.size].pack('N') + x2
15
+ end.join
16
+ end
17
+
18
+ def to_a
19
+ @args.collect{|x| x.to_a}
20
+ end
21
+
22
+ private
23
+
24
+ def encode_timetag(t)
25
+ case t
26
+ when nil # immediately
27
+ t1 = 0
28
+ t2 = 1
29
+ when Numeric
30
+ t1, t2 = construct_timetag(t)
31
+ when Time
32
+ t1, t2 = construct_timetag(t.to_ntp)
33
+ else
34
+ raise(ArgumentError, 'invalid time')
35
+ end
36
+ [t1, t2].pack('N2')
37
+ end
38
+
39
+ def construct_timetag(time)
40
+ t1, fr = time.divmod(1)
41
+ t2 = (fr * (2**32)).to_i
42
+
43
+ [t1, t2]
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ module OSC
2
+ class Client
3
+
4
+ def initialize(host, port)
5
+ @so = UDPSocket.new
6
+ @so.connect(host, port)
7
+ end
8
+
9
+ def send(mesg)
10
+ @so.send(mesg.encode, 0)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ class Time
2
+
3
+ def to_ntp
4
+ self.to_f + 2208988800
5
+ end
6
+ end
@@ -0,0 +1,63 @@
1
+ require 'eventmachine'
2
+
3
+ module OSC
4
+ Channel = EM::Channel.new
5
+
6
+ class Connection < EventMachine::Connection
7
+ def receive_data(data)
8
+ ip_info = get_peername[2,6].unpack("nC4")
9
+ Channel << OSC::OSCPacket.messages_from_network(data, ip_info)
10
+ end
11
+ end
12
+
13
+ class EMServer
14
+
15
+ def initialize(port = 3333)
16
+ @port = port
17
+ setup_dispatcher
18
+ @tuples = []
19
+ end
20
+
21
+ def run
22
+ EM.error_handler{ |e|
23
+ Thread.main.raise e
24
+ }
25
+
26
+ EM.run do
27
+ EM::open_datagram_socket "0.0.0.0", @port, Connection
28
+ end
29
+ end
30
+
31
+ def add_method(address_pattern, &proc)
32
+ matcher = AddressPattern.new(address_pattern)
33
+
34
+ @tuples << [matcher, proc]
35
+ end
36
+
37
+ private
38
+ def setup_dispatcher
39
+ Channel.subscribe do |messages|
40
+ messages.each do |message|
41
+ diff = (message.time || 0) - Time.now.to_ntp
42
+
43
+ if diff <= 0
44
+ sendmesg(message)
45
+ else
46
+ EM.defer do
47
+ sleep(diff)
48
+ sendmesg(message)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def sendmesg(mesg)
56
+ @tuples.each do |matcher, obj|
57
+ if matcher.match?(mesg.address)
58
+ obj.call(mesg)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,46 @@
1
+ module OSC
2
+ class Message
3
+ attr_accessor :address
4
+ attr_accessor :time
5
+ attr_accessor :ip_address
6
+ attr_accessor :ip_port
7
+
8
+ def self.new_with_time(address, time, tags=nil, *args)
9
+ message = new(address, tags, *args)
10
+ message.time = time
11
+ message
12
+ end
13
+
14
+ def initialize(address, *args)
15
+ @address = address
16
+ @args = []
17
+
18
+ args.each do |arg|
19
+ case arg
20
+ when Integer; @args << OSCInt32.new(arg)
21
+ when Float; @args << OSCFloat32.new(arg)
22
+ when String; @args << OSCString.new(arg)
23
+ when OSCArgument; @args << arg
24
+ end
25
+ end
26
+ end
27
+
28
+ def tags
29
+ @args.collect{|x| x.tag}.join
30
+ end
31
+
32
+ def encode
33
+ s = OSCString.new(@address).encode
34
+ s << OSCString.new(',' + tags).encode
35
+ s << @args.collect{|x| x.encode}.join
36
+ end
37
+
38
+ def to_a
39
+ @args.collect{|x| x.val}
40
+ end
41
+
42
+ def ==(other)
43
+ @address == other.address && to_a == other.to_a
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,42 @@
1
+ module OSC
2
+ class NetworkPacket
3
+ def initialize(str)
4
+ @str = str.force_encoding("BINARY")
5
+ @index = 0
6
+ end
7
+
8
+ def to_s
9
+ @str
10
+ end
11
+
12
+ def rem
13
+ @str.length - @index
14
+ end
15
+
16
+ def eof?
17
+ rem <= 0
18
+ end
19
+
20
+ def skip(n)
21
+ @index += n
22
+ end
23
+
24
+ def skip_padding
25
+ skip((4 - (@index % 4)) % 4)
26
+ end
27
+
28
+ def getn(n)
29
+ raise(EOFError) if rem < n
30
+ s = @str[@index, n]
31
+ skip(n)
32
+ s
33
+ end
34
+
35
+ def getc
36
+ raise(EOFError) if rem < 1
37
+ c = @str[@index]
38
+ skip(1)
39
+ c
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,26 @@
1
+ module OSC
2
+ class OSCArgument
3
+ def initialize(val)
4
+ @val = val
5
+ end
6
+
7
+ attr_accessor :val
8
+
9
+ def to_i()
10
+ @val.to_i
11
+ end
12
+
13
+ def to_f()
14
+ @val.to_f
15
+ end
16
+
17
+ def to_s()
18
+ @val.to_s
19
+ end
20
+
21
+ private
22
+ def padding(s)
23
+ s + ("\000" * ((4 - (s.size % 4)) % 4))
24
+ end
25
+ end
26
+ end