rubino-agent 0.3.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 (376) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +115 -0
  4. data/.rubocop_todo.yml +955 -0
  5. data/.ruby-version +1 -0
  6. data/AGENTS.md +97 -0
  7. data/CHANGELOG.md +344 -0
  8. data/CONTRIBUTING.md +69 -0
  9. data/LICENSE +21 -0
  10. data/README.md +200 -0
  11. data/Rakefile +8 -0
  12. data/docs/agents.md +190 -0
  13. data/docs/api/v1.md +414 -0
  14. data/docs/architecture.md +177 -0
  15. data/docs/commands.md +375 -0
  16. data/docs/configuration.md +590 -0
  17. data/docs/getting-started.md +143 -0
  18. data/docs/jobs.md +332 -0
  19. data/docs/mcp.md +128 -0
  20. data/docs/memory.md +98 -0
  21. data/docs/models-and-keys.md +173 -0
  22. data/docs/oauth-providers.md +145 -0
  23. data/docs/plugins.md +195 -0
  24. data/docs/security.md +145 -0
  25. data/docs/skills.md +322 -0
  26. data/docs/tools.md +395 -0
  27. data/docs/troubleshooting.md +73 -0
  28. data/exe/rubino +9 -0
  29. data/install.sh +275 -0
  30. data/lib/rubino/active_skill.rb +50 -0
  31. data/lib/rubino/agent/agent_registry.rb +120 -0
  32. data/lib/rubino/agent/backoff_policy.rb +116 -0
  33. data/lib/rubino/agent/definition.rb +128 -0
  34. data/lib/rubino/agent/degenerate_recovery.rb +271 -0
  35. data/lib/rubino/agent/fallback_chain.rb +194 -0
  36. data/lib/rubino/agent/iteration_budget.rb +50 -0
  37. data/lib/rubino/agent/loop.rb +617 -0
  38. data/lib/rubino/agent/model_call_runner.rb +383 -0
  39. data/lib/rubino/agent/prompts/build.txt +69 -0
  40. data/lib/rubino/agent/prompts/compaction.txt +20 -0
  41. data/lib/rubino/agent/prompts/explore.txt +19 -0
  42. data/lib/rubino/agent/prompts/general.txt +20 -0
  43. data/lib/rubino/agent/prompts/plan.txt +31 -0
  44. data/lib/rubino/agent/response_validator.rb +70 -0
  45. data/lib/rubino/agent/router.rb +65 -0
  46. data/lib/rubino/agent/runner.rb +195 -0
  47. data/lib/rubino/agent/tool_executor.rb +402 -0
  48. data/lib/rubino/agent/truncation_continuation.rb +137 -0
  49. data/lib/rubino/api/middleware/auth.rb +43 -0
  50. data/lib/rubino/api/middleware/error_handler.rb +65 -0
  51. data/lib/rubino/api/middleware/json_parser.rb +100 -0
  52. data/lib/rubino/api/middleware/observability.rb +59 -0
  53. data/lib/rubino/api/middleware/rate_limit.rb +136 -0
  54. data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
  55. data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
  56. data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
  57. data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
  58. data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
  59. data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
  60. data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
  61. data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
  62. data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
  63. data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
  64. data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
  65. data/lib/rubino/api/operations/files/read_operation.rb +40 -0
  66. data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
  67. data/lib/rubino/api/operations/health_operation.rb +46 -0
  68. data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
  69. data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
  70. data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
  71. data/lib/rubino/api/operations/metrics_operation.rb +18 -0
  72. data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
  73. data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
  74. data/lib/rubino/api/operations/models/list_operation.rb +45 -0
  75. data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
  76. data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
  77. data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
  78. data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
  79. data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
  80. data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
  81. data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
  82. data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
  83. data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
  84. data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
  85. data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
  86. data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
  87. data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
  88. data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
  89. data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
  90. data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
  91. data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
  92. data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
  93. data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
  94. data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
  95. data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
  96. data/lib/rubino/api/request.rb +54 -0
  97. data/lib/rubino/api/responses.rb +64 -0
  98. data/lib/rubino/api/router.rb +72 -0
  99. data/lib/rubino/api/schemas.rb +103 -0
  100. data/lib/rubino/api/server.rb +102 -0
  101. data/lib/rubino/api/tls.rb +108 -0
  102. data/lib/rubino/attachments/classification.rb +16 -0
  103. data/lib/rubino/attachments/classify.rb +171 -0
  104. data/lib/rubino/attachments/defang.rb +47 -0
  105. data/lib/rubino/attachments/policy.rb +36 -0
  106. data/lib/rubino/attachments/preamble.rb +120 -0
  107. data/lib/rubino/boot/encryption_key.rb +32 -0
  108. data/lib/rubino/cli/chat/bang_shell.rb +257 -0
  109. data/lib/rubino/cli/chat/completion_builder.rb +290 -0
  110. data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
  111. data/lib/rubino/cli/chat/image_inbox.rb +168 -0
  112. data/lib/rubino/cli/chat/session_resolver.rb +176 -0
  113. data/lib/rubino/cli/chat_command.rb +1674 -0
  114. data/lib/rubino/cli/commands.rb +250 -0
  115. data/lib/rubino/cli/config_command.rb +96 -0
  116. data/lib/rubino/cli/doctor_command.rb +251 -0
  117. data/lib/rubino/cli/jobs_command.rb +60 -0
  118. data/lib/rubino/cli/memory_command.rb +135 -0
  119. data/lib/rubino/cli/onboarding_wizard.rb +207 -0
  120. data/lib/rubino/cli/server_command.rb +139 -0
  121. data/lib/rubino/cli/session_command.rb +125 -0
  122. data/lib/rubino/cli/setup_command.rb +107 -0
  123. data/lib/rubino/cli/skills_command.rb +85 -0
  124. data/lib/rubino/cli/tools_command.rb +81 -0
  125. data/lib/rubino/cli/trust_gate.rb +71 -0
  126. data/lib/rubino/commands/built_ins.rb +46 -0
  127. data/lib/rubino/commands/command.rb +116 -0
  128. data/lib/rubino/commands/executor.rb +550 -0
  129. data/lib/rubino/commands/handlers/agents.rb +510 -0
  130. data/lib/rubino/commands/handlers/config.rb +88 -0
  131. data/lib/rubino/commands/handlers/help.rb +148 -0
  132. data/lib/rubino/commands/handlers/jobs.rb +71 -0
  133. data/lib/rubino/commands/handlers/mcp.rb +229 -0
  134. data/lib/rubino/commands/handlers/memory.rb +200 -0
  135. data/lib/rubino/commands/handlers/sessions.rb +207 -0
  136. data/lib/rubino/commands/handlers/skills.rb +195 -0
  137. data/lib/rubino/commands/handlers/status.rb +211 -0
  138. data/lib/rubino/commands/loader.rb +90 -0
  139. data/lib/rubino/config/configuration.rb +455 -0
  140. data/lib/rubino/config/defaults.rb +569 -0
  141. data/lib/rubino/config/loader.rb +115 -0
  142. data/lib/rubino/config/reasoning_prefs.rb +67 -0
  143. data/lib/rubino/config/writer.rb +72 -0
  144. data/lib/rubino/context/compressor.rb +149 -0
  145. data/lib/rubino/context/environment_inspector.rb +176 -0
  146. data/lib/rubino/context/file_discovery.rb +45 -0
  147. data/lib/rubino/context/message_boundary.rb +39 -0
  148. data/lib/rubino/context/prompt_assembler.rb +382 -0
  149. data/lib/rubino/context/summary_builder.rb +159 -0
  150. data/lib/rubino/context/token_budget.rb +68 -0
  151. data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
  152. data/lib/rubino/database/connection.rb +77 -0
  153. data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
  154. data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
  155. data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
  156. data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
  157. data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
  158. data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
  159. data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
  160. data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
  161. data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
  162. data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
  163. data/lib/rubino/database/migrator.rb +48 -0
  164. data/lib/rubino/documents/converters/csv.rb +79 -0
  165. data/lib/rubino/documents/converters/docx.rb +129 -0
  166. data/lib/rubino/documents/converters/html.rb +28 -0
  167. data/lib/rubino/documents/converters/json.rb +35 -0
  168. data/lib/rubino/documents/converters/pdf.rb +59 -0
  169. data/lib/rubino/documents/converters/plain.rb +68 -0
  170. data/lib/rubino/documents/converters/pptx.rb +64 -0
  171. data/lib/rubino/documents/converters/xlsx.rb +62 -0
  172. data/lib/rubino/documents/converters/xml.rb +45 -0
  173. data/lib/rubino/documents/html.rb +71 -0
  174. data/lib/rubino/documents/registry.rb +68 -0
  175. data/lib/rubino/documents/table.rb +63 -0
  176. data/lib/rubino/documents.rb +50 -0
  177. data/lib/rubino/errors.rb +119 -0
  178. data/lib/rubino/files/workspace.rb +93 -0
  179. data/lib/rubino/interaction/cancel_token.rb +43 -0
  180. data/lib/rubino/interaction/clipboard_image.rb +84 -0
  181. data/lib/rubino/interaction/event_bus.rb +48 -0
  182. data/lib/rubino/interaction/events.rb +101 -0
  183. data/lib/rubino/interaction/image_input.rb +127 -0
  184. data/lib/rubino/interaction/input_queue.rb +117 -0
  185. data/lib/rubino/interaction/lifecycle.rb +299 -0
  186. data/lib/rubino/interaction/probe.rb +65 -0
  187. data/lib/rubino/interaction/state.rb +56 -0
  188. data/lib/rubino/jobs/cron_job_repository.rb +75 -0
  189. data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
  190. data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
  191. data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
  192. data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
  193. data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
  194. data/lib/rubino/jobs/queue.rb +184 -0
  195. data/lib/rubino/jobs/registry.rb +45 -0
  196. data/lib/rubino/jobs/runner.rb +79 -0
  197. data/lib/rubino/jobs/scheduler.rb +138 -0
  198. data/lib/rubino/jobs/webhook_delivery.rb +225 -0
  199. data/lib/rubino/jobs/worker.rb +59 -0
  200. data/lib/rubino/llm/adapter_factory.rb +47 -0
  201. data/lib/rubino/llm/adapter_response.rb +65 -0
  202. data/lib/rubino/llm/auxiliary_client.rb +61 -0
  203. data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
  204. data/lib/rubino/llm/content_builder.rb +55 -0
  205. data/lib/rubino/llm/credential_check.rb +93 -0
  206. data/lib/rubino/llm/error_classifier.rb +364 -0
  207. data/lib/rubino/llm/fake_provider.rb +292 -0
  208. data/lib/rubino/llm/inline_think_filter.rb +58 -0
  209. data/lib/rubino/llm/model_catalog.rb +29 -0
  210. data/lib/rubino/llm/provider_resolver.rb +48 -0
  211. data/lib/rubino/llm/reasoning_manager.rb +100 -0
  212. data/lib/rubino/llm/request.rb +56 -0
  213. data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
  214. data/lib/rubino/llm/scenario_loader.rb +68 -0
  215. data/lib/rubino/llm/scenario_selector.rb +80 -0
  216. data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
  217. data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
  218. data/lib/rubino/llm/scenarios/analysis.yml +501 -0
  219. data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
  220. data/lib/rubino/llm/scenarios/failure.yml +65 -0
  221. data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
  222. data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
  223. data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
  224. data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
  225. data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
  226. data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
  227. data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
  228. data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
  229. data/lib/rubino/llm/thinking_support.rb +84 -0
  230. data/lib/rubino/llm/tool_bridge.rb +89 -0
  231. data/lib/rubino/logger.rb +99 -0
  232. data/lib/rubino/mcp/manager.rb +180 -0
  233. data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
  234. data/lib/rubino/mcp.rb +57 -0
  235. data/lib/rubino/memory/backend.rb +104 -0
  236. data/lib/rubino/memory/backends/default.rb +101 -0
  237. data/lib/rubino/memory/backends/sqlite.rb +653 -0
  238. data/lib/rubino/memory/backends.rb +53 -0
  239. data/lib/rubino/memory/deduplicator.rb +74 -0
  240. data/lib/rubino/memory/extractor.rb +85 -0
  241. data/lib/rubino/memory/flusher.rb +31 -0
  242. data/lib/rubino/memory/retriever.rb +50 -0
  243. data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
  244. data/lib/rubino/memory/sqlite_graph.rb +154 -0
  245. data/lib/rubino/memory/store.rb +228 -0
  246. data/lib/rubino/memory/threat_scanner.rb +68 -0
  247. data/lib/rubino/metrics.rb +175 -0
  248. data/lib/rubino/modes.rb +93 -0
  249. data/lib/rubino/oauth/connection_repository.rb +95 -0
  250. data/lib/rubino/oauth/provider/github.rb +75 -0
  251. data/lib/rubino/oauth/provider/google.rb +59 -0
  252. data/lib/rubino/oauth/provider.rb +149 -0
  253. data/lib/rubino/oauth/registry.rb +86 -0
  254. data/lib/rubino/oauth/token_encryptor.rb +87 -0
  255. data/lib/rubino/plugins/registry.rb +75 -0
  256. data/lib/rubino/plugins.rb +86 -0
  257. data/lib/rubino/run/approval_gate.rb +243 -0
  258. data/lib/rubino/run/attachment_downloader.rb +166 -0
  259. data/lib/rubino/run/event_store.rb +74 -0
  260. data/lib/rubino/run/executor.rb +383 -0
  261. data/lib/rubino/run/gate_registry.rb +39 -0
  262. data/lib/rubino/run/recorder.rb +69 -0
  263. data/lib/rubino/run/repository.rb +118 -0
  264. data/lib/rubino/run/session_approval_cache.rb +118 -0
  265. data/lib/rubino/security/allowlist_persister.rb +55 -0
  266. data/lib/rubino/security/approval_policy.rb +227 -0
  267. data/lib/rubino/security/command_allowlist.rb +24 -0
  268. data/lib/rubino/security/dangerous_patterns.rb +118 -0
  269. data/lib/rubino/security/deny_persister.rb +73 -0
  270. data/lib/rubino/security/doom_loop_detector.rb +43 -0
  271. data/lib/rubino/security/hardline_guard.rb +105 -0
  272. data/lib/rubino/security/pattern_matcher.rb +62 -0
  273. data/lib/rubino/security/prefix_deriver.rb +124 -0
  274. data/lib/rubino/security/readonly_commands.rb +211 -0
  275. data/lib/rubino/session/exporter.rb +101 -0
  276. data/lib/rubino/session/message.rb +77 -0
  277. data/lib/rubino/session/repository.rb +295 -0
  278. data/lib/rubino/session/store.rb +198 -0
  279. data/lib/rubino/session/summary_store.rb +65 -0
  280. data/lib/rubino/skills/prompt_index.rb +85 -0
  281. data/lib/rubino/skills/registry.rb +208 -0
  282. data/lib/rubino/skills/skill.rb +176 -0
  283. data/lib/rubino/skills/skill_tool.rb +215 -0
  284. data/lib/rubino/skills/state_repository.rb +37 -0
  285. data/lib/rubino/skills/toggle.rb +26 -0
  286. data/lib/rubino/tools/answer_child_tool.rb +83 -0
  287. data/lib/rubino/tools/ask_parent_tool.rb +232 -0
  288. data/lib/rubino/tools/attach_file_tool.rb +120 -0
  289. data/lib/rubino/tools/background_tasks.rb +520 -0
  290. data/lib/rubino/tools/base.rb +222 -0
  291. data/lib/rubino/tools/custom_tool_loader.rb +119 -0
  292. data/lib/rubino/tools/edit_tool.rb +122 -0
  293. data/lib/rubino/tools/git_tool.rb +71 -0
  294. data/lib/rubino/tools/github_tool.rb +233 -0
  295. data/lib/rubino/tools/glob_tool.rb +69 -0
  296. data/lib/rubino/tools/grep_tool.rb +206 -0
  297. data/lib/rubino/tools/memory_tool.rb +184 -0
  298. data/lib/rubino/tools/multi_edit_tool.rb +110 -0
  299. data/lib/rubino/tools/patch_tool.rb +260 -0
  300. data/lib/rubino/tools/probe_tool.rb +175 -0
  301. data/lib/rubino/tools/question_tool.rb +128 -0
  302. data/lib/rubino/tools/read_attachment_tool.rb +180 -0
  303. data/lib/rubino/tools/read_tool.rb +212 -0
  304. data/lib/rubino/tools/read_tracker.rb +98 -0
  305. data/lib/rubino/tools/registry.rb +166 -0
  306. data/lib/rubino/tools/result.rb +113 -0
  307. data/lib/rubino/tools/ruby_tool.rb +0 -0
  308. data/lib/rubino/tools/session_search_tool.rb +103 -0
  309. data/lib/rubino/tools/shell_input_tool.rb +96 -0
  310. data/lib/rubino/tools/shell_kill_tool.rb +76 -0
  311. data/lib/rubino/tools/shell_output_tool.rb +72 -0
  312. data/lib/rubino/tools/shell_registry.rb +158 -0
  313. data/lib/rubino/tools/shell_tail_tool.rb +118 -0
  314. data/lib/rubino/tools/shell_tool.rb +330 -0
  315. data/lib/rubino/tools/steer_tool.rb +118 -0
  316. data/lib/rubino/tools/subagent_probe.rb +89 -0
  317. data/lib/rubino/tools/summarize_file_tool.rb +182 -0
  318. data/lib/rubino/tools/task_result_tool.rb +90 -0
  319. data/lib/rubino/tools/task_stop_tool.rb +80 -0
  320. data/lib/rubino/tools/task_tool.rb +622 -0
  321. data/lib/rubino/tools/test_tool.rb +454 -0
  322. data/lib/rubino/tools/todo_tool.rb +93 -0
  323. data/lib/rubino/tools/tool_call_repository.rb +33 -0
  324. data/lib/rubino/tools/vision_tool.rb +85 -0
  325. data/lib/rubino/tools/webfetch_tool.rb +153 -0
  326. data/lib/rubino/tools/websearch_tool.rb +179 -0
  327. data/lib/rubino/tools/write_tool.rb +61 -0
  328. data/lib/rubino/trust.rb +88 -0
  329. data/lib/rubino/ui/api.rb +296 -0
  330. data/lib/rubino/ui/base.rb +252 -0
  331. data/lib/rubino/ui/bottom_composer.rb +1599 -0
  332. data/lib/rubino/ui/cli.rb +1987 -0
  333. data/lib/rubino/ui/completion_menu.rb +321 -0
  334. data/lib/rubino/ui/completion_source.rb +284 -0
  335. data/lib/rubino/ui/escape_reader.rb +169 -0
  336. data/lib/rubino/ui/indented_io.rb +88 -0
  337. data/lib/rubino/ui/input_history.rb +108 -0
  338. data/lib/rubino/ui/live_region.rb +183 -0
  339. data/lib/rubino/ui/markdown_renderer.rb +506 -0
  340. data/lib/rubino/ui/notifier.rb +163 -0
  341. data/lib/rubino/ui/null.rb +195 -0
  342. data/lib/rubino/ui/paste_store.rb +176 -0
  343. data/lib/rubino/ui/printer_base.rb +79 -0
  344. data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
  345. data/lib/rubino/ui/queued_indicators.rb +66 -0
  346. data/lib/rubino/ui/status_bar.rb +100 -0
  347. data/lib/rubino/ui/stdout_proxy.rb +161 -0
  348. data/lib/rubino/ui/streaming_markdown.rb +186 -0
  349. data/lib/rubino/ui/subagent_cards.rb +134 -0
  350. data/lib/rubino/ui/subagent_view.rb +255 -0
  351. data/lib/rubino/ui.rb +21 -0
  352. data/lib/rubino/update_check.rb +187 -0
  353. data/lib/rubino/util/duration.rb +23 -0
  354. data/lib/rubino/util/hyperlink.rb +105 -0
  355. data/lib/rubino/util/output.rb +145 -0
  356. data/lib/rubino/util/secrets_mask.rb +83 -0
  357. data/lib/rubino/version.rb +5 -0
  358. data/lib/rubino/workspace.rb +85 -0
  359. data/lib/rubino-agent.rb +5 -0
  360. data/lib/rubino.rb +318 -0
  361. data/mise.toml +2 -0
  362. data/rubino-agent.gemspec +103 -0
  363. data/skills/ruby-expert/SKILL.md +67 -0
  364. data/skills/ruby-expert/references/concurrency.md +357 -0
  365. data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
  366. data/skills/ruby-expert/references/errors-and-types.md +460 -0
  367. data/skills/ruby-expert/references/gem-authoring.md +459 -0
  368. data/skills/ruby-expert/references/language-idioms.md +465 -0
  369. data/skills/ruby-expert/references/metaprogramming.md +339 -0
  370. data/skills/ruby-expert/references/oo-design.md +553 -0
  371. data/skills/ruby-expert/references/performance.md +383 -0
  372. data/skills/ruby-expert/references/rails.md +424 -0
  373. data/skills/ruby-expert/references/security.md +404 -0
  374. data/skills/ruby-expert/references/testing.md +473 -0
  375. data/skills/ruby-expert/references/tooling.md +466 -0
  376. metadata +856 -0
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Rubino
6
+ module CLI
7
+ # Handles initial setup: creates config directory, default config,
8
+ # initializes the database, and runs migrations.
9
+ class SetupCommand
10
+ def execute
11
+ ui = Rubino.ui
12
+
13
+ ui.info("Setting up rubino...")
14
+ ui.blank_line
15
+
16
+ # Create home directory (0700 — only the owner sees stored secrets)
17
+ # and subdirectories. ensure_directories! owns the mkdir + chmod so
18
+ # every entry point that materializes the home agrees on 0700 (#65).
19
+ home = Rubino.home_path
20
+ Rubino.ensure_directories!
21
+ ui.success("Home directory: #{home}")
22
+ ui.success("Subdirectories created")
23
+
24
+ # Create config file if it doesn't exist
25
+ loader = Config::Loader.new
26
+ if loader.config_exists?
27
+ ui.warning("Config already exists: #{loader.config_path}")
28
+ else
29
+ loader.create_default_config!
30
+ File.chmod(0o600, loader.config_path)
31
+ ui.success("Config created: #{loader.config_path}")
32
+ end
33
+
34
+ # Create .env template if it doesn't exist (0600 — contains api keys)
35
+ env_path = File.join(home, ".env")
36
+ unless File.exist?(env_path)
37
+ File.write(env_path, env_template)
38
+ File.chmod(0o600, env_path)
39
+ ui.success("Env template created: #{env_path}")
40
+ end
41
+
42
+ # Initialize database
43
+ ui.status("Initializing database...")
44
+ connection = Rubino.database
45
+ migrator = Database::Migrator.new(connection)
46
+ migrator.migrate!
47
+ ui.success("Database initialized: #{connection.db_path}")
48
+
49
+ # First-run onboarding: if no usable key is configured yet AND we're on
50
+ # a real TTY, guide the user to a working model (provider/model/key)
51
+ # right here so `setup` ends in a usable config — not a dead-end that
52
+ # still needs hand-editing config.yml (#93). Non-interactive setup keeps
53
+ # the old behaviour (files created, no prompts).
54
+ maybe_run_onboarding(ui)
55
+
56
+ ui.blank_line
57
+ # Tell the truth about the end state (#31). A green "Setup complete!" is
58
+ # only honest when a usable credential is actually configured — printing
59
+ # it after a skipped/abandoned onboarding (no provider, no key) directly
60
+ # contradicts the state. Re-check the credential after onboarding so the
61
+ # final line reflects reality on both the interactive and the
62
+ # non-interactive (files-only) paths.
63
+ if LLM::CredentialCheck.usable?
64
+ ui.success("Setup complete! Run 'rubino doctor' to verify.")
65
+ else
66
+ ui.warning("Setup files created, but no model is configured yet.")
67
+ ui.status("Run 'rubino setup' again or add an API key, then 'rubino doctor' to verify.")
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def maybe_run_onboarding(ui)
74
+ return unless interactive?
75
+ return if LLM::CredentialCheck.usable?
76
+
77
+ OnboardingWizard.new(ui: ui).run
78
+ end
79
+
80
+ def interactive?
81
+ $stdin.tty? && $stdout.tty?
82
+ rescue StandardError
83
+ false
84
+ end
85
+
86
+ def env_template
87
+ <<~ENV
88
+ # Rubino API Keys
89
+ # Add your API keys here. Do NOT commit this file.
90
+ # `rubino setup` (on a terminal) can fill one in for you.
91
+
92
+ # MiniMax (recommended default — Anthropic-compatible)
93
+ # MINIMAX_API_KEY=...
94
+
95
+ # OpenAI
96
+ # OPENAI_API_KEY=sk-...
97
+
98
+ # Anthropic
99
+ # ANTHROPIC_API_KEY=sk-ant-...
100
+
101
+ # Google
102
+ # GEMINI_API_KEY=...
103
+ ENV
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Rubino
6
+ module CLI
7
+ # Subcommands for managing skills (#188). `list` mirrors the in-chat
8
+ # /skills disclosure (enabled/disabled markers), `show` prints a skill's
9
+ # SKILL.md body (trust review before enabling), and `enable`/`disable`
10
+ # run the SAME registry-validated StateRepository write the HTTP API
11
+ # toggle and the in-chat `/skills enable|disable` use (Skills::Toggle) —
12
+ # no new logic, just the missing terminal surface.
13
+ class SkillsCommand < Thor
14
+ # Clean `tree`/help label instead of the underscored class-name default (F12).
15
+ namespace "rubino skills"
16
+
17
+ def self.exit_on_failure?
18
+ true
19
+ end
20
+
21
+ desc "list", "List skills with enabled/disabled markers"
22
+ def list
23
+ Rubino.ensure_database_ready!
24
+ registry = Skills::Registry.trusted
25
+ skills = registry.all
26
+ if skills.empty?
27
+ Rubino.ui.info("No skills found.")
28
+ Rubino.ui.info("Add .md files to .rubino/skills/ to create skills.")
29
+ return
30
+ end
31
+
32
+ rows = skills.map do |skill|
33
+ [skill.name, skill_status(skill.name, registry), skill.description.to_s]
34
+ end
35
+ Rubino.ui.table(headers: %w[Name Status Description], rows: rows)
36
+ end
37
+
38
+ desc "show NAME", "Print a skill's SKILL.md body (review it before enabling)"
39
+ def show(name)
40
+ skill = Skills::Registry.trusted.find(name)
41
+ if skill.nil?
42
+ Rubino.ui.error("unknown skill: #{name}")
43
+ return
44
+ end
45
+
46
+ Rubino.ui.info(skill.content)
47
+ end
48
+
49
+ desc "enable NAME", "Enable a skill (back into the index, every session)"
50
+ def enable(name)
51
+ toggle(name, enabled: true)
52
+ end
53
+
54
+ desc "disable NAME", "Disable a skill (drops out of the index, every session)"
55
+ def disable(name)
56
+ toggle(name, enabled: false)
57
+ end
58
+
59
+ private
60
+
61
+ # The Status cell: enabled/disabled from the StateRepository (the same
62
+ # source the in-chat list's "(disabled)" marker reads), plus the active
63
+ # pin when this process carries one (the slot is process-level, so a
64
+ # fresh CLI run normally shows none — the marker matters in-process).
65
+ def skill_status(name, registry)
66
+ status = registry.enabled?(name) ? "enabled" : "disabled"
67
+ status += " · active" if Rubino::ActiveSkill.current == name
68
+ status
69
+ end
70
+
71
+ def toggle(name, enabled:)
72
+ Rubino.ensure_database_ready!
73
+ registry = Skills::Registry.trusted
74
+ unless Skills::Toggle.set(name, enabled: enabled, registry: registry)
75
+ Rubino.ui.error("unknown skill: #{name}")
76
+ available = registry.names
77
+ Rubino.ui.info("Available: #{available.join(", ")}") unless available.empty?
78
+ return
79
+ end
80
+
81
+ Rubino.ui.success("#{enabled ? "Enabled" : "Disabled"} skill: #{name}")
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module CLI
5
+ # Lists available tools and their status
6
+ class ToolsCommand
7
+ def execute
8
+ ui = Rubino.ui
9
+ config = Rubino.configuration
10
+
11
+ ui.info("Available Tools:")
12
+ ui.blank_line
13
+
14
+ # The registry is populated lazily when an agent runner boots; the bare
15
+ # `rubino tools` command never boots one, so without this the table
16
+ # is empty (F6). Registering the defaults is idempotent and matches what
17
+ # ChatCommand#ensure_setup! does before a turn.
18
+ Tools::Registry.register_defaults! if Tools::Registry.all.empty?
19
+
20
+ # Report against the SAME config gate the registry enforces: each row
21
+ # is a `tools.<config_key>` group, resolved exactly like
22
+ # Registry#tool_enabled_in_config? (opt-out — absent key = enabled).
23
+ # Deriving the rows from the registered tools' #config_key (rather
24
+ # than a hardcoded list) means the displayed state can never drift
25
+ # from reality — `web` no longer shows "disabled" while webfetch/
26
+ # websearch stay live, and the dead `browser` key is gone.
27
+ # MCP wrappers are excluded here: they are dynamic (no `tools.<key>`
28
+ # config gate) and get their own section below instead of fake rows
29
+ # in the config-group table.
30
+ builtins = Tools::Registry.all.grep_v(MCP::MCPToolWrapper)
31
+ config_keys = builtins.map(&:config_key).uniq
32
+ rows = config_keys.sort.map do |key|
33
+ value = config.dig("tools", key)
34
+ enabled = value.nil? || value == true
35
+ [key, enabled ? "enabled" : "disabled"]
36
+ end
37
+
38
+ ui.table(headers: %w[Tool Status], rows: rows)
39
+
40
+ print_enable_hint(rows)
41
+ print_mcp_tools
42
+ end
43
+
44
+ private
45
+
46
+ # A disabled row with no pointer is a dead end (#20): name the exact
47
+ # config command that turns the group back on.
48
+ def print_enable_hint(rows)
49
+ disabled = rows.select { |_, status| status == "disabled" }.map(&:first)
50
+ return if disabled.empty?
51
+
52
+ ui = Rubino.ui
53
+ ui.blank_line
54
+ ui.info("Enable with: rubino config set tools.<name> true (e.g. tools.#{disabled.first})")
55
+ end
56
+
57
+ # Lists tools from configured MCP servers (#91). Configuring
58
+ # `mcp.servers` is the opt-in: the Manager connects, prefixes each tool
59
+ # `servername_toolname`, and registers it alongside the built-ins. A
60
+ # configured-but-empty result still prints a breadcrumb (#94) so MCP
61
+ # users are never left staring at a silently builtin-only table —
62
+ # unreachable servers additionally warn via Manager#start_server.
63
+ def print_mcp_tools
64
+ return unless MCP.enabled?
65
+
66
+ ui = Rubino.ui
67
+ ui.blank_line
68
+ ui.info("MCP Tools (experimental):")
69
+ MCP.boot!
70
+
71
+ mcp_tools = Tools::Registry.all.grep(MCP::MCPToolWrapper)
72
+ if mcp_tools.empty?
73
+ ui.warning("mcp.servers configured, but no MCP tools loaded")
74
+ else
75
+ rows = mcp_tools.map { |t| [t.name, t.server_name] }.sort
76
+ ui.table(headers: %w[Tool Server], rows: rows)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module CLI
5
+ # The interactive folder-trust checkpoint. Asks ONCE per directory, before
6
+ # that directory's AGENTS.md / project context + .rubino/skills are honored,
7
+ # and remembers the answer in Rubino::Trust so it's never re-asked.
8
+ #
9
+ # Modelled on VS Code Workspace Trust + Claude Code's trust dialog. Declining
10
+ # is non-destructive (VS Code "Restricted Mode"): the session still runs, it
11
+ # just runs WITHOUT that directory's project context/skills (the assembler
12
+ # consults Rubino::Trust.trusted? before injecting them).
13
+ #
14
+ # Skipped entirely — no prompt, treated as allowed for the duration — when:
15
+ # - the dir is already trusted,
16
+ # - the dir has nothing to gate (no context file, no .rubino/skills),
17
+ # - --ignore-rules was passed (project context is off regardless), or
18
+ # - the run is non-interactive (-q / no TTY): we never block automation.
19
+ class TrustGate
20
+ def initialize(ui: nil, interactive: true, ignore_rules: false)
21
+ @ui = ui || Rubino.ui
22
+ @interactive = interactive
23
+ @ignore_rules = ignore_rules
24
+ end
25
+
26
+ # Ensures +dir+ has a trust decision. Returns true when the directory's
27
+ # project context/skills may be loaded, false when it must run in
28
+ # restricted mode. Prompts at most once, then remembers a "yes".
29
+ def ensure_trust(dir)
30
+ return true if Rubino::Trust.trusted?(dir)
31
+ return true if @ignore_rules # context already suppressed
32
+ return true unless gateworthy?(dir) # nothing to gate → no ceremony
33
+
34
+ # Non-interactive: never block. We also do NOT remember it (the user
35
+ # never vouched), so context stays withheld this run — Restricted Mode
36
+ # by default for automation, matching VS Code's headless behaviour.
37
+ return false unless @interactive
38
+
39
+ prompt(dir)
40
+ end
41
+
42
+ private
43
+
44
+ # Asks the one-time question and records the answer. Default is No.
45
+ def prompt(dir)
46
+ @ui.blank_line if @ui.respond_to?(:blank_line)
47
+ @ui.info("▸ Starting in #{dir} — its AGENTS.md and project skills will shape the agent.")
48
+ answer = @ui.ask(" Trust this folder? [y/N] ").to_s.strip.downcase
49
+
50
+ if answer.start_with?("y")
51
+ Rubino::Trust.remember(dir)
52
+ @ui.success("Trusted #{dir} — loading its project context and skills.") if @ui.respond_to?(:success)
53
+ true
54
+ else
55
+ @ui.info("Running in restricted mode — #{dir}'s AGENTS.md and skills will NOT be loaded.")
56
+ false
57
+ end
58
+ end
59
+
60
+ # A directory is only worth a trust prompt when it actually ships
61
+ # something rubino auto-injects: a project-context file or a skills dir.
62
+ # An empty scratch dir gets no prompt — there's nothing to be steered by.
63
+ def gateworthy?(dir)
64
+ Context::FileDiscovery.new(base_path: dir).discover_files.any? ||
65
+ File.directory?(File.join(dir, Skills::PromptIndex::DEFAULT_SKILL_DIR))
66
+ rescue StandardError
67
+ false
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module Commands
5
+ # Single source of truth for built-in slash command names + descriptions.
6
+ # Referenced by the CLI UI for tab-completion and by the Executor for
7
+ # `/help` and the unknown-command "Available" list (so both stay in sync —
8
+ # previously `/help` omitted /quit and /mode, L7).
9
+ module BuiltIns
10
+ # Ordered: name => one-line description shown by `/help`.
11
+ DESCRIPTIONS = {
12
+ "/status" => "Overview: model, mode, session, memory, background work",
13
+ "/sessions" => "List recent sessions; resume, show, or delete one (--all lifts the cap)",
14
+ "/new" => "Start a fresh session (the current one is left intact)",
15
+ "/clear" => "Alias for /new — start a fresh session",
16
+ "/probe" => "Ask an ephemeral side-question (not saved); tip: start a line with '? '",
17
+ "/queued" => "Queue a message to run after the current turn (Alt+Enter does the same)",
18
+ "/branch" => "Fork the current session into a new one and switch into it",
19
+ "/compact" => "Compact the context now: older turns become a summary",
20
+ "/export" => "Write the session transcript as markdown (/export [path])",
21
+ "/memory" => "Inspect/search/forget what the agent remembers (show ID, backend, --all)",
22
+ "/agents" => "List background subagents; steer/probe a running one, or view output",
23
+ "/tasks" => "Alias for /agents",
24
+ "/reply" => "Answer a subagent that is blocked waiting on you (ask_parent)",
25
+ "/jobs" => "List the background job queue (status counts); /jobs <id> for detail",
26
+ "/skills" => "List skills; activate one ('none' clears), or enable/disable NAME",
27
+ "/mcp" => "List MCP servers and their tools; restart or disable one",
28
+ "/add-dir" => "Add an extra allowed workspace directory (write/edit can reach it)",
29
+ "/dirs" => "List the current workspace roots",
30
+ "/config" => "Read or set configuration (/config <key> [value]; 'show' = full view)",
31
+ "/model" => "Show or switch the model for this session (/model <name>)",
32
+ "/mode" => "Show or switch mode (default | plan | yolo)",
33
+ "/reasoning" => "Show or switch how reasoning is shown (hidden | collapsed | full)",
34
+ "/think" => "Show or switch thinking effort (off | low | medium | high)",
35
+ "/commands" => "List custom commands (and how to make them)",
36
+ "/help" => "Show this help",
37
+ "/paste" => "Attach an image from the clipboard",
38
+ "/clear-images" => "Drop pending image attachments",
39
+ "/exit" => "End session",
40
+ "/quit" => "End session"
41
+ }.freeze
42
+
43
+ NAMES = DESCRIPTIONS.keys.freeze
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Rubino
6
+ module Commands
7
+ # Represents a custom slash command loaded from a Markdown file.
8
+ # Supports $ARGUMENTS, $1-$9 positional params, @file refs.
9
+ #
10
+ # Shell injection via !`command` is opt-in and disabled by default.
11
+ # Enable it by setting commands.shell_injection_enabled: true in your
12
+ # configuration — only do so in trusted, controlled environments.
13
+ class Command
14
+ attr_reader :name, :description, :agent, :model, :path
15
+
16
+ def initialize(path:)
17
+ @path = path
18
+ @metadata = {}
19
+ @template = nil
20
+ parse!
21
+ end
22
+
23
+ # Renders the command prompt with given arguments.
24
+ def render(arguments = "")
25
+ prompt = template.dup
26
+
27
+ substitute_arguments!(prompt, arguments)
28
+ process_shell_injections!(prompt)
29
+ process_file_references!(prompt)
30
+
31
+ prompt.strip
32
+ end
33
+
34
+ # Returns the raw template content.
35
+ def template
36
+ @template ||= load_template
37
+ end
38
+
39
+ private
40
+
41
+ # Replace $ARGUMENTS and positional $1..$9 params.
42
+ def substitute_arguments!(prompt, arguments)
43
+ prompt.gsub!("$ARGUMENTS", arguments)
44
+
45
+ args = arguments.split(/\s+/)
46
+ (1..9).each do |i|
47
+ prompt.gsub!("$#{i}", args[i - 1] || "")
48
+ end
49
+ end
50
+
51
+ # Process !`command` shell injections — only when explicitly enabled.
52
+ def process_shell_injections!(prompt)
53
+ return unless shell_injection_enabled?
54
+
55
+ prompt.gsub!(/!`([^`]+)`/) do
56
+ command = Regexp.last_match(1)
57
+ `#{command} 2>&1`.strip
58
+ end
59
+ end
60
+
61
+ # Replace @path/to/file references with file content.
62
+ def process_file_references!(prompt)
63
+ prompt.gsub!(%r{@([\w/._-]+)}) do
64
+ file_path = Regexp.last_match(1)
65
+ expanded = File.expand_path(file_path)
66
+ if File.exist?(expanded)
67
+ File.read(expanded)
68
+ else
69
+ "@#{file_path} (file not found)"
70
+ end
71
+ end
72
+ end
73
+
74
+ def shell_injection_enabled?
75
+ Rubino.configuration.dig("commands", "shell_injection_enabled") == true
76
+ end
77
+
78
+ def parse!
79
+ raw = File.read(@path)
80
+
81
+ if raw.start_with?("---")
82
+ parts = raw.split("---", 3)
83
+ if parts.size >= 3
84
+ begin
85
+ @metadata = YAML.safe_load(parts[1], permitted_classes: [Symbol]) || {}
86
+ rescue Psych::SyntaxError => e
87
+ warn "rubino: skipping malformed frontmatter in #{@path} " \
88
+ "(line #{e.line}: #{e.problem}); treating whole file as template"
89
+ @metadata = {}
90
+ @template = raw
91
+ end
92
+ unless @metadata.is_a?(Hash)
93
+ warn "rubino: ignoring non-Hash frontmatter in #{@path}; treating whole file as template"
94
+ @metadata = {}
95
+ @template = raw
96
+ end
97
+ @template ||= parts[2].strip
98
+ else
99
+ @template = raw
100
+ end
101
+ else
102
+ @template = raw
103
+ end
104
+
105
+ @name = (@metadata["name"] || File.basename(@path, ".md")).to_s
106
+ @description = @metadata["description"] || ""
107
+ @agent = @metadata["agent"]
108
+ @model = @metadata["model"]
109
+ end
110
+
111
+ def load_template
112
+ @template
113
+ end
114
+ end
115
+ end
116
+ end