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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +115 -0
- data/.rubocop_todo.yml +955 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +97 -0
- data/CHANGELOG.md +344 -0
- data/CONTRIBUTING.md +69 -0
- data/LICENSE +21 -0
- data/README.md +200 -0
- data/Rakefile +8 -0
- data/docs/agents.md +190 -0
- data/docs/api/v1.md +414 -0
- data/docs/architecture.md +177 -0
- data/docs/commands.md +375 -0
- data/docs/configuration.md +590 -0
- data/docs/getting-started.md +143 -0
- data/docs/jobs.md +332 -0
- data/docs/mcp.md +128 -0
- data/docs/memory.md +98 -0
- data/docs/models-and-keys.md +173 -0
- data/docs/oauth-providers.md +145 -0
- data/docs/plugins.md +195 -0
- data/docs/security.md +145 -0
- data/docs/skills.md +322 -0
- data/docs/tools.md +395 -0
- data/docs/troubleshooting.md +73 -0
- data/exe/rubino +9 -0
- data/install.sh +275 -0
- data/lib/rubino/active_skill.rb +50 -0
- data/lib/rubino/agent/agent_registry.rb +120 -0
- data/lib/rubino/agent/backoff_policy.rb +116 -0
- data/lib/rubino/agent/definition.rb +128 -0
- data/lib/rubino/agent/degenerate_recovery.rb +271 -0
- data/lib/rubino/agent/fallback_chain.rb +194 -0
- data/lib/rubino/agent/iteration_budget.rb +50 -0
- data/lib/rubino/agent/loop.rb +617 -0
- data/lib/rubino/agent/model_call_runner.rb +383 -0
- data/lib/rubino/agent/prompts/build.txt +69 -0
- data/lib/rubino/agent/prompts/compaction.txt +20 -0
- data/lib/rubino/agent/prompts/explore.txt +19 -0
- data/lib/rubino/agent/prompts/general.txt +20 -0
- data/lib/rubino/agent/prompts/plan.txt +31 -0
- data/lib/rubino/agent/response_validator.rb +70 -0
- data/lib/rubino/agent/router.rb +65 -0
- data/lib/rubino/agent/runner.rb +195 -0
- data/lib/rubino/agent/tool_executor.rb +402 -0
- data/lib/rubino/agent/truncation_continuation.rb +137 -0
- data/lib/rubino/api/middleware/auth.rb +43 -0
- data/lib/rubino/api/middleware/error_handler.rb +65 -0
- data/lib/rubino/api/middleware/json_parser.rb +100 -0
- data/lib/rubino/api/middleware/observability.rb +59 -0
- data/lib/rubino/api/middleware/rate_limit.rb +136 -0
- data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
- data/lib/rubino/api/operations/files/read_operation.rb +40 -0
- data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
- data/lib/rubino/api/operations/health_operation.rb +46 -0
- data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
- data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
- data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
- data/lib/rubino/api/operations/metrics_operation.rb +18 -0
- data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
- data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
- data/lib/rubino/api/operations/models/list_operation.rb +45 -0
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
- data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
- data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
- data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
- data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
- data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
- data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
- data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
- data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
- data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
- data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
- data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
- data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
- data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
- data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
- data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
- data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
- data/lib/rubino/api/request.rb +54 -0
- data/lib/rubino/api/responses.rb +64 -0
- data/lib/rubino/api/router.rb +72 -0
- data/lib/rubino/api/schemas.rb +103 -0
- data/lib/rubino/api/server.rb +102 -0
- data/lib/rubino/api/tls.rb +108 -0
- data/lib/rubino/attachments/classification.rb +16 -0
- data/lib/rubino/attachments/classify.rb +171 -0
- data/lib/rubino/attachments/defang.rb +47 -0
- data/lib/rubino/attachments/policy.rb +36 -0
- data/lib/rubino/attachments/preamble.rb +120 -0
- data/lib/rubino/boot/encryption_key.rb +32 -0
- data/lib/rubino/cli/chat/bang_shell.rb +257 -0
- data/lib/rubino/cli/chat/completion_builder.rb +290 -0
- data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
- data/lib/rubino/cli/chat/image_inbox.rb +168 -0
- data/lib/rubino/cli/chat/session_resolver.rb +176 -0
- data/lib/rubino/cli/chat_command.rb +1674 -0
- data/lib/rubino/cli/commands.rb +250 -0
- data/lib/rubino/cli/config_command.rb +96 -0
- data/lib/rubino/cli/doctor_command.rb +251 -0
- data/lib/rubino/cli/jobs_command.rb +60 -0
- data/lib/rubino/cli/memory_command.rb +135 -0
- data/lib/rubino/cli/onboarding_wizard.rb +207 -0
- data/lib/rubino/cli/server_command.rb +139 -0
- data/lib/rubino/cli/session_command.rb +125 -0
- data/lib/rubino/cli/setup_command.rb +107 -0
- data/lib/rubino/cli/skills_command.rb +85 -0
- data/lib/rubino/cli/tools_command.rb +81 -0
- data/lib/rubino/cli/trust_gate.rb +71 -0
- data/lib/rubino/commands/built_ins.rb +46 -0
- data/lib/rubino/commands/command.rb +116 -0
- data/lib/rubino/commands/executor.rb +550 -0
- data/lib/rubino/commands/handlers/agents.rb +510 -0
- data/lib/rubino/commands/handlers/config.rb +88 -0
- data/lib/rubino/commands/handlers/help.rb +148 -0
- data/lib/rubino/commands/handlers/jobs.rb +71 -0
- data/lib/rubino/commands/handlers/mcp.rb +229 -0
- data/lib/rubino/commands/handlers/memory.rb +200 -0
- data/lib/rubino/commands/handlers/sessions.rb +207 -0
- data/lib/rubino/commands/handlers/skills.rb +195 -0
- data/lib/rubino/commands/handlers/status.rb +211 -0
- data/lib/rubino/commands/loader.rb +90 -0
- data/lib/rubino/config/configuration.rb +455 -0
- data/lib/rubino/config/defaults.rb +569 -0
- data/lib/rubino/config/loader.rb +115 -0
- data/lib/rubino/config/reasoning_prefs.rb +67 -0
- data/lib/rubino/config/writer.rb +72 -0
- data/lib/rubino/context/compressor.rb +149 -0
- data/lib/rubino/context/environment_inspector.rb +176 -0
- data/lib/rubino/context/file_discovery.rb +45 -0
- data/lib/rubino/context/message_boundary.rb +39 -0
- data/lib/rubino/context/prompt_assembler.rb +382 -0
- data/lib/rubino/context/summary_builder.rb +159 -0
- data/lib/rubino/context/token_budget.rb +68 -0
- data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
- data/lib/rubino/database/connection.rb +77 -0
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
- data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
- data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
- data/lib/rubino/database/migrator.rb +48 -0
- data/lib/rubino/documents/converters/csv.rb +79 -0
- data/lib/rubino/documents/converters/docx.rb +129 -0
- data/lib/rubino/documents/converters/html.rb +28 -0
- data/lib/rubino/documents/converters/json.rb +35 -0
- data/lib/rubino/documents/converters/pdf.rb +59 -0
- data/lib/rubino/documents/converters/plain.rb +68 -0
- data/lib/rubino/documents/converters/pptx.rb +64 -0
- data/lib/rubino/documents/converters/xlsx.rb +62 -0
- data/lib/rubino/documents/converters/xml.rb +45 -0
- data/lib/rubino/documents/html.rb +71 -0
- data/lib/rubino/documents/registry.rb +68 -0
- data/lib/rubino/documents/table.rb +63 -0
- data/lib/rubino/documents.rb +50 -0
- data/lib/rubino/errors.rb +119 -0
- data/lib/rubino/files/workspace.rb +93 -0
- data/lib/rubino/interaction/cancel_token.rb +43 -0
- data/lib/rubino/interaction/clipboard_image.rb +84 -0
- data/lib/rubino/interaction/event_bus.rb +48 -0
- data/lib/rubino/interaction/events.rb +101 -0
- data/lib/rubino/interaction/image_input.rb +127 -0
- data/lib/rubino/interaction/input_queue.rb +117 -0
- data/lib/rubino/interaction/lifecycle.rb +299 -0
- data/lib/rubino/interaction/probe.rb +65 -0
- data/lib/rubino/interaction/state.rb +56 -0
- data/lib/rubino/jobs/cron_job_repository.rb +75 -0
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
- data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
- data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
- data/lib/rubino/jobs/queue.rb +184 -0
- data/lib/rubino/jobs/registry.rb +45 -0
- data/lib/rubino/jobs/runner.rb +79 -0
- data/lib/rubino/jobs/scheduler.rb +138 -0
- data/lib/rubino/jobs/webhook_delivery.rb +225 -0
- data/lib/rubino/jobs/worker.rb +59 -0
- data/lib/rubino/llm/adapter_factory.rb +47 -0
- data/lib/rubino/llm/adapter_response.rb +65 -0
- data/lib/rubino/llm/auxiliary_client.rb +61 -0
- data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
- data/lib/rubino/llm/content_builder.rb +55 -0
- data/lib/rubino/llm/credential_check.rb +93 -0
- data/lib/rubino/llm/error_classifier.rb +364 -0
- data/lib/rubino/llm/fake_provider.rb +292 -0
- data/lib/rubino/llm/inline_think_filter.rb +58 -0
- data/lib/rubino/llm/model_catalog.rb +29 -0
- data/lib/rubino/llm/provider_resolver.rb +48 -0
- data/lib/rubino/llm/reasoning_manager.rb +100 -0
- data/lib/rubino/llm/request.rb +56 -0
- data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
- data/lib/rubino/llm/scenario_loader.rb +68 -0
- data/lib/rubino/llm/scenario_selector.rb +80 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
- data/lib/rubino/llm/scenarios/analysis.yml +501 -0
- data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
- data/lib/rubino/llm/scenarios/failure.yml +65 -0
- data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
- data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
- data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
- data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
- data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
- data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
- data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
- data/lib/rubino/llm/thinking_support.rb +84 -0
- data/lib/rubino/llm/tool_bridge.rb +89 -0
- data/lib/rubino/logger.rb +99 -0
- data/lib/rubino/mcp/manager.rb +180 -0
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
- data/lib/rubino/mcp.rb +57 -0
- data/lib/rubino/memory/backend.rb +104 -0
- data/lib/rubino/memory/backends/default.rb +101 -0
- data/lib/rubino/memory/backends/sqlite.rb +653 -0
- data/lib/rubino/memory/backends.rb +53 -0
- data/lib/rubino/memory/deduplicator.rb +74 -0
- data/lib/rubino/memory/extractor.rb +85 -0
- data/lib/rubino/memory/flusher.rb +31 -0
- data/lib/rubino/memory/retriever.rb +50 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
- data/lib/rubino/memory/sqlite_graph.rb +154 -0
- data/lib/rubino/memory/store.rb +228 -0
- data/lib/rubino/memory/threat_scanner.rb +68 -0
- data/lib/rubino/metrics.rb +175 -0
- data/lib/rubino/modes.rb +93 -0
- data/lib/rubino/oauth/connection_repository.rb +95 -0
- data/lib/rubino/oauth/provider/github.rb +75 -0
- data/lib/rubino/oauth/provider/google.rb +59 -0
- data/lib/rubino/oauth/provider.rb +149 -0
- data/lib/rubino/oauth/registry.rb +86 -0
- data/lib/rubino/oauth/token_encryptor.rb +87 -0
- data/lib/rubino/plugins/registry.rb +75 -0
- data/lib/rubino/plugins.rb +86 -0
- data/lib/rubino/run/approval_gate.rb +243 -0
- data/lib/rubino/run/attachment_downloader.rb +166 -0
- data/lib/rubino/run/event_store.rb +74 -0
- data/lib/rubino/run/executor.rb +383 -0
- data/lib/rubino/run/gate_registry.rb +39 -0
- data/lib/rubino/run/recorder.rb +69 -0
- data/lib/rubino/run/repository.rb +118 -0
- data/lib/rubino/run/session_approval_cache.rb +118 -0
- data/lib/rubino/security/allowlist_persister.rb +55 -0
- data/lib/rubino/security/approval_policy.rb +227 -0
- data/lib/rubino/security/command_allowlist.rb +24 -0
- data/lib/rubino/security/dangerous_patterns.rb +118 -0
- data/lib/rubino/security/deny_persister.rb +73 -0
- data/lib/rubino/security/doom_loop_detector.rb +43 -0
- data/lib/rubino/security/hardline_guard.rb +105 -0
- data/lib/rubino/security/pattern_matcher.rb +62 -0
- data/lib/rubino/security/prefix_deriver.rb +124 -0
- data/lib/rubino/security/readonly_commands.rb +211 -0
- data/lib/rubino/session/exporter.rb +101 -0
- data/lib/rubino/session/message.rb +77 -0
- data/lib/rubino/session/repository.rb +295 -0
- data/lib/rubino/session/store.rb +198 -0
- data/lib/rubino/session/summary_store.rb +65 -0
- data/lib/rubino/skills/prompt_index.rb +85 -0
- data/lib/rubino/skills/registry.rb +208 -0
- data/lib/rubino/skills/skill.rb +176 -0
- data/lib/rubino/skills/skill_tool.rb +215 -0
- data/lib/rubino/skills/state_repository.rb +37 -0
- data/lib/rubino/skills/toggle.rb +26 -0
- data/lib/rubino/tools/answer_child_tool.rb +83 -0
- data/lib/rubino/tools/ask_parent_tool.rb +232 -0
- data/lib/rubino/tools/attach_file_tool.rb +120 -0
- data/lib/rubino/tools/background_tasks.rb +520 -0
- data/lib/rubino/tools/base.rb +222 -0
- data/lib/rubino/tools/custom_tool_loader.rb +119 -0
- data/lib/rubino/tools/edit_tool.rb +122 -0
- data/lib/rubino/tools/git_tool.rb +71 -0
- data/lib/rubino/tools/github_tool.rb +233 -0
- data/lib/rubino/tools/glob_tool.rb +69 -0
- data/lib/rubino/tools/grep_tool.rb +206 -0
- data/lib/rubino/tools/memory_tool.rb +184 -0
- data/lib/rubino/tools/multi_edit_tool.rb +110 -0
- data/lib/rubino/tools/patch_tool.rb +260 -0
- data/lib/rubino/tools/probe_tool.rb +175 -0
- data/lib/rubino/tools/question_tool.rb +128 -0
- data/lib/rubino/tools/read_attachment_tool.rb +180 -0
- data/lib/rubino/tools/read_tool.rb +212 -0
- data/lib/rubino/tools/read_tracker.rb +98 -0
- data/lib/rubino/tools/registry.rb +166 -0
- data/lib/rubino/tools/result.rb +113 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/session_search_tool.rb +103 -0
- data/lib/rubino/tools/shell_input_tool.rb +96 -0
- data/lib/rubino/tools/shell_kill_tool.rb +76 -0
- data/lib/rubino/tools/shell_output_tool.rb +72 -0
- data/lib/rubino/tools/shell_registry.rb +158 -0
- data/lib/rubino/tools/shell_tail_tool.rb +118 -0
- data/lib/rubino/tools/shell_tool.rb +330 -0
- data/lib/rubino/tools/steer_tool.rb +118 -0
- data/lib/rubino/tools/subagent_probe.rb +89 -0
- data/lib/rubino/tools/summarize_file_tool.rb +182 -0
- data/lib/rubino/tools/task_result_tool.rb +90 -0
- data/lib/rubino/tools/task_stop_tool.rb +80 -0
- data/lib/rubino/tools/task_tool.rb +622 -0
- data/lib/rubino/tools/test_tool.rb +454 -0
- data/lib/rubino/tools/todo_tool.rb +93 -0
- data/lib/rubino/tools/tool_call_repository.rb +33 -0
- data/lib/rubino/tools/vision_tool.rb +85 -0
- data/lib/rubino/tools/webfetch_tool.rb +153 -0
- data/lib/rubino/tools/websearch_tool.rb +179 -0
- data/lib/rubino/tools/write_tool.rb +61 -0
- data/lib/rubino/trust.rb +88 -0
- data/lib/rubino/ui/api.rb +296 -0
- data/lib/rubino/ui/base.rb +252 -0
- data/lib/rubino/ui/bottom_composer.rb +1599 -0
- data/lib/rubino/ui/cli.rb +1987 -0
- data/lib/rubino/ui/completion_menu.rb +321 -0
- data/lib/rubino/ui/completion_source.rb +284 -0
- data/lib/rubino/ui/escape_reader.rb +169 -0
- data/lib/rubino/ui/indented_io.rb +88 -0
- data/lib/rubino/ui/input_history.rb +108 -0
- data/lib/rubino/ui/live_region.rb +183 -0
- data/lib/rubino/ui/markdown_renderer.rb +506 -0
- data/lib/rubino/ui/notifier.rb +163 -0
- data/lib/rubino/ui/null.rb +195 -0
- data/lib/rubino/ui/paste_store.rb +176 -0
- data/lib/rubino/ui/printer_base.rb +79 -0
- data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
- data/lib/rubino/ui/queued_indicators.rb +66 -0
- data/lib/rubino/ui/status_bar.rb +100 -0
- data/lib/rubino/ui/stdout_proxy.rb +161 -0
- data/lib/rubino/ui/streaming_markdown.rb +186 -0
- data/lib/rubino/ui/subagent_cards.rb +134 -0
- data/lib/rubino/ui/subagent_view.rb +255 -0
- data/lib/rubino/ui.rb +21 -0
- data/lib/rubino/update_check.rb +187 -0
- data/lib/rubino/util/duration.rb +23 -0
- data/lib/rubino/util/hyperlink.rb +105 -0
- data/lib/rubino/util/output.rb +145 -0
- data/lib/rubino/util/secrets_mask.rb +83 -0
- data/lib/rubino/version.rb +5 -0
- data/lib/rubino/workspace.rb +85 -0
- data/lib/rubino-agent.rb +5 -0
- data/lib/rubino.rb +318 -0
- data/mise.toml +2 -0
- data/rubino-agent.gemspec +103 -0
- data/skills/ruby-expert/SKILL.md +67 -0
- data/skills/ruby-expert/references/concurrency.md +357 -0
- data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
- data/skills/ruby-expert/references/errors-and-types.md +460 -0
- data/skills/ruby-expert/references/gem-authoring.md +459 -0
- data/skills/ruby-expert/references/language-idioms.md +465 -0
- data/skills/ruby-expert/references/metaprogramming.md +339 -0
- data/skills/ruby-expert/references/oo-design.md +553 -0
- data/skills/ruby-expert/references/performance.md +383 -0
- data/skills/ruby-expert/references/rails.md +424 -0
- data/skills/ruby-expert/references/security.md +404 -0
- data/skills/ruby-expert/references/testing.md +473 -0
- data/skills/ruby-expert/references/tooling.md +466 -0
- metadata +856 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module CLI
|
|
7
|
+
# Main Thor command class. All subcommands are registered here.
|
|
8
|
+
class Commands < Thor
|
|
9
|
+
# Without an explicit namespace, Thor's `tree` command derives one by
|
|
10
|
+
# underscoring the class name — "Rubino::CLI::Commands" becomes the
|
|
11
|
+
# mangled "rubino:c_l_i:commands" (the CLI acronym splits into
|
|
12
|
+
# c_l_i) (F12/F14). Pin a clean label instead.
|
|
13
|
+
namespace "rubino"
|
|
14
|
+
|
|
15
|
+
def self.exit_on_failure?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Allow passing prompt directly as default task:
|
|
20
|
+
# rubino "my prompt"
|
|
21
|
+
def self.default_command
|
|
22
|
+
:chat
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Help flags recognized on any top-level command (#134).
|
|
26
|
+
HELP_FLAGS = ["--help", "-h"].freeze
|
|
27
|
+
|
|
28
|
+
# Intercept `--version`/`-v` at dispatch (#32). Thor otherwise routes a
|
|
29
|
+
# bare `rubino --version` to the default `chat` task, which treats the
|
|
30
|
+
# flag as a prompt and fails with an API-key error. Handle it here —
|
|
31
|
+
# print the version and exit — before any chat/credential handling.
|
|
32
|
+
#
|
|
33
|
+
# Likewise intercept `rubino <command> --help` (#134): Thor 1.x only maps
|
|
34
|
+
# a LEADING help flag to the help task, so `chat --help`/`prompt --help`
|
|
35
|
+
# used to fall through as an unknown option, become the positional
|
|
36
|
+
# prompt, and start a REAL agent run (provider call + memory writes).
|
|
37
|
+
# Reroute to Thor's own `help <command>` before option parsing. Thor
|
|
38
|
+
# subcommands (config/memory/sessions/jobs) already handle their own
|
|
39
|
+
# `--help` and keep their richer subcommand listing.
|
|
40
|
+
def self.start(given_args = ARGV, config = {})
|
|
41
|
+
if ["--version", "-v"].include?(given_args.first)
|
|
42
|
+
puts "rubino v#{Rubino::VERSION}"
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
cmd = given_args.first.to_s.tr("-", "_")
|
|
47
|
+
if given_args.drop(1).intersect?(HELP_FLAGS) && commands.key?(cmd) && !subcommands.include?(cmd)
|
|
48
|
+
return super(["help", cmd], config)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Wrap subcommand help so `chat --help` / `prompt --help` stay within 80
|
|
55
|
+
# columns (#217). Thor's stock #print_options lays the flags and their
|
|
56
|
+
# descriptions out in a 2-column table padded to the WIDEST flag — and the
|
|
57
|
+
# boolean variants (`[--no-x], [--skip-x]`) push that column past 60, so
|
|
58
|
+
# every description row overflowed 80 (the longest hit 137) with no
|
|
59
|
+
# wrapping. Render each option as its flag line followed by the
|
|
60
|
+
# description wrapped + indented on its own line(s) instead: bounded by
|
|
61
|
+
# construction, and it reads cleaner than the ragged padded table.
|
|
62
|
+
HELP_WRAP_COLUMNS = 80
|
|
63
|
+
HELP_DESC_INDENT = 6
|
|
64
|
+
|
|
65
|
+
def self.print_options(shell, options, group_name = nil)
|
|
66
|
+
return if options.empty?
|
|
67
|
+
|
|
68
|
+
shell.say(group_name ? "#{group_name} options:" : "Options:")
|
|
69
|
+
options.reject(&:hide).each do |option|
|
|
70
|
+
shell.say(" #{option.usage(0)}")
|
|
71
|
+
next unless option.description
|
|
72
|
+
|
|
73
|
+
wrap_help_description(option.description).each { |line| shell.say(line) }
|
|
74
|
+
end
|
|
75
|
+
shell.say ""
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Greedy word-wrap of a flag description to HELP_WRAP_COLUMNS, each line
|
|
79
|
+
# indented HELP_DESC_INDENT. Wrapped here (not via Thor's print_wrapped)
|
|
80
|
+
# so the bound is the fixed 80 the spec checks, not the live terminal
|
|
81
|
+
# width. A single word longer than the budget is emitted on its own line
|
|
82
|
+
# rather than dropped.
|
|
83
|
+
def self.wrap_help_description(description)
|
|
84
|
+
indent = " " * HELP_DESC_INDENT
|
|
85
|
+
budget = HELP_WRAP_COLUMNS - HELP_DESC_INDENT
|
|
86
|
+
lines = []
|
|
87
|
+
line = +""
|
|
88
|
+
description.to_s.split(/\s+/).each do |word|
|
|
89
|
+
if line.empty?
|
|
90
|
+
line << word
|
|
91
|
+
elsif line.length + 1 + word.length <= budget
|
|
92
|
+
line << " " << word
|
|
93
|
+
else
|
|
94
|
+
lines << (indent + line)
|
|
95
|
+
line = +word
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
lines << (indent + line) unless line.empty?
|
|
99
|
+
lines
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
desc "setup", "Initialize rubino configuration and database"
|
|
103
|
+
def setup
|
|
104
|
+
SetupCommand.new.execute
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# ----------------------------------------------------------------
|
|
108
|
+
# chat — interactive and non-interactive
|
|
109
|
+
# ----------------------------------------------------------------
|
|
110
|
+
desc "chat [PROMPT]", "Chat with the agent (one-shot with -q)"
|
|
111
|
+
|
|
112
|
+
# One-shot / non-interactive
|
|
113
|
+
option :query, aliases: "-q", type: :string, desc: "One-shot prompt (non-interactive)"
|
|
114
|
+
|
|
115
|
+
# Attach image(s) to the turn's native vision slot. Repeatable:
|
|
116
|
+
# --image a.png --image b.jpg.
|
|
117
|
+
# A single-value, repeatable string (not a greedy array) so a trailing
|
|
118
|
+
# positional prompt — `--image pic.png "what is this?"` — stays the prompt
|
|
119
|
+
# instead of being swallowed as a second image. Works in both one-shot
|
|
120
|
+
# (-q) and interactive mode; @image tokens in the prompt itself are also
|
|
121
|
+
# honoured. Aligns with `llm`'s -a/--attachment.
|
|
122
|
+
option :image, aliases: "-i", type: :string, repeatable: true,
|
|
123
|
+
desc: "Attach image file to the prompt (repeatable)"
|
|
124
|
+
|
|
125
|
+
# Session management
|
|
126
|
+
option :session, aliases: "-s", type: :string, desc: "Resume session by ID"
|
|
127
|
+
option :resume, aliases: "-r", type: :string, desc: "Resume session by ID or title"
|
|
128
|
+
option :continue, aliases: "-c", type: :boolean, desc: "Resume most recent session"
|
|
129
|
+
option :new, type: :boolean,
|
|
130
|
+
desc: "Start a fresh session (bare `chat` resumes the last one by default)"
|
|
131
|
+
|
|
132
|
+
# Model / provider
|
|
133
|
+
option :model, aliases: "-m", type: :string, desc: "Override model (e.g. claude-sonnet-4-5)"
|
|
134
|
+
option :provider, type: :string, desc: "Override provider (e.g. bedrock, anthropic)"
|
|
135
|
+
|
|
136
|
+
# Behavior
|
|
137
|
+
option :yolo, type: :boolean, desc: "Skip all approval prompts"
|
|
138
|
+
option :max_turns, type: :numeric, desc: "Max tool iterations per turn"
|
|
139
|
+
option :ignore_rules, type: :boolean, desc: "Skip AGENTS.md and context files"
|
|
140
|
+
|
|
141
|
+
# Add extra allowed workspace roots at launch (repeatable), like Claude
|
|
142
|
+
# Code's --add-dir. Write/edit tools then accept files under any added
|
|
143
|
+
# root; an added dir's project context/skills are gated by folder-trust.
|
|
144
|
+
option :add_dir, type: :string, repeatable: true,
|
|
145
|
+
desc: "Add an extra allowed workspace directory (repeatable)"
|
|
146
|
+
|
|
147
|
+
def chat(prompt = nil)
|
|
148
|
+
# Support: rubino chat "prompt" as shorthand for -q
|
|
149
|
+
opts = options.to_h.merge(prompt ? { query: prompt } : {})
|
|
150
|
+
ChatCommand.new(opts).execute
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# ----------------------------------------------------------------
|
|
154
|
+
# Shorthand: rubino prompt "my question"
|
|
155
|
+
# ----------------------------------------------------------------
|
|
156
|
+
desc "prompt PROMPT", "Run a one-shot prompt (alias for chat -q)"
|
|
157
|
+
option :model, aliases: "-m", type: :string, desc: "Override model"
|
|
158
|
+
option :provider, type: :string, desc: "Override provider"
|
|
159
|
+
option :image, aliases: "-i", type: :string, repeatable: true, desc: "Attach image file (repeatable)"
|
|
160
|
+
option :session, aliases: "-s", type: :string, desc: "Session ID to resume"
|
|
161
|
+
option :continue, aliases: "-c", type: :boolean, desc: "Resume most recent session"
|
|
162
|
+
option :resume, aliases: "-r", type: :string, desc: "Resume by ID or title"
|
|
163
|
+
option :yolo, type: :boolean, desc: "Skip approval prompts"
|
|
164
|
+
option :max_turns, type: :numeric, desc: "Max tool iterations"
|
|
165
|
+
option :ignore_rules, type: :boolean, desc: "Skip AGENTS.md/context files"
|
|
166
|
+
option :add_dir, type: :string, repeatable: true,
|
|
167
|
+
desc: "Add an extra allowed workspace directory (repeatable)"
|
|
168
|
+
def prompt(*args)
|
|
169
|
+
query = args.join(" ")
|
|
170
|
+
opts = options.to_h.merge(query: query)
|
|
171
|
+
ChatCommand.new(opts).execute
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
desc "config SUBCOMMAND", "Manage configuration"
|
|
175
|
+
subcommand "config", ConfigCommand
|
|
176
|
+
|
|
177
|
+
desc "memory SUBCOMMAND", "Manage persistent memories"
|
|
178
|
+
subcommand "memory", MemoryCommand
|
|
179
|
+
|
|
180
|
+
desc "sessions SUBCOMMAND", "Manage chat sessions"
|
|
181
|
+
subcommand "sessions", SessionCommand
|
|
182
|
+
|
|
183
|
+
desc "jobs SUBCOMMAND", "Manage background jobs"
|
|
184
|
+
subcommand "jobs", JobsCommand
|
|
185
|
+
|
|
186
|
+
desc "skills SUBCOMMAND", "Manage skills (list, show, enable, disable)"
|
|
187
|
+
subcommand "skills", SkillsCommand
|
|
188
|
+
|
|
189
|
+
desc "tools", "List available tools"
|
|
190
|
+
def tools
|
|
191
|
+
ToolsCommand.new.execute
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
desc "server", "Start the JSON API server"
|
|
195
|
+
option :port, type: :numeric, default: 4820, desc: "Port to listen on"
|
|
196
|
+
option :host, type: :string, desc: "Host/interface to bind (default 127.0.0.1; pass 0.0.0.0 to expose)"
|
|
197
|
+
option :api_key, type: :string, desc: "Bearer token required on every request"
|
|
198
|
+
def server
|
|
199
|
+
ServerCommand.new(options).execute
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# The usage label matches the registered command name (tls_cert) so
|
|
203
|
+
# `--help` and `tree` render the SAME name (#20); Thor still dispatches
|
|
204
|
+
# the hyphenated spelling (`rubino tls-cert`) via its name normalization.
|
|
205
|
+
desc "tls_cert", "Print the self-signed TLS certificate PEM"
|
|
206
|
+
def tls_cert
|
|
207
|
+
$stdout.write(API::TLS.ensure_cert!)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
desc "doctor", "Check system health"
|
|
211
|
+
def doctor
|
|
212
|
+
DoctorCommand.new.execute
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
desc "version", "Show version"
|
|
216
|
+
def version
|
|
217
|
+
Rubino.ui.info("rubino v#{Rubino::VERSION}")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
desc "update", "Update rubino to the latest published version"
|
|
221
|
+
def update
|
|
222
|
+
ui = Rubino.ui
|
|
223
|
+
current = Rubino::VERSION
|
|
224
|
+
|
|
225
|
+
case Rubino::UpdateCheck.install_method
|
|
226
|
+
when :gem
|
|
227
|
+
ok = system(*Rubino::UpdateCheck.gem_update_command)
|
|
228
|
+
unless ok
|
|
229
|
+
ui.warning("gem update failed. If this is a permission error, re-run the installer or try `gem update --user-install #{Rubino::UpdateCheck::GEM_NAME}`.")
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
new_v = Rubino::UpdateCheck.installed_gem_version(Rubino::UpdateCheck::GEM_NAME)
|
|
233
|
+
if new_v && Gem::Version.new(new_v) > Gem::Version.new(current)
|
|
234
|
+
ui.info("rubino is now on v#{new_v} (was v#{current}).")
|
|
235
|
+
ui.status("Restart any running rubino sessions to pick up the new version.")
|
|
236
|
+
else
|
|
237
|
+
ui.info("rubino is already up to date (v#{current}).")
|
|
238
|
+
end
|
|
239
|
+
else
|
|
240
|
+
ui.warning("rubino wasn't installed from RubyGems (built from source / dev checkout).")
|
|
241
|
+
ui.status("Re-run the installer to update:")
|
|
242
|
+
ui.status(" curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh | bash")
|
|
243
|
+
end
|
|
244
|
+
ensure
|
|
245
|
+
# Drop the cached notice so the boot footer doesn't linger after update.
|
|
246
|
+
Rubino::UpdateCheck.clear_cache!
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module CLI
|
|
7
|
+
# Subcommands for managing configuration
|
|
8
|
+
class ConfigCommand < Thor
|
|
9
|
+
# Clean `tree`/help label instead of the underscored
|
|
10
|
+
# "rubino:c_l_i:config_command" Thor derives from the class name (F12).
|
|
11
|
+
namespace "rubino config"
|
|
12
|
+
|
|
13
|
+
def self.exit_on_failure?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "get KEY", "Get a configuration value (dot-notation; secrets masked)"
|
|
18
|
+
def get(key)
|
|
19
|
+
self.class.render_get(key, ui: Rubino.ui)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# ONE get rendering for both surfaces (#187): this CLI verb and the
|
|
23
|
+
# in-chat `/config get` (Commands::Executor). Resolves against the
|
|
24
|
+
# effective config (file merged over defaults), the same source `show`
|
|
25
|
+
# and the running agent use, so default-valued keys are returned instead
|
|
26
|
+
# of falsely reported "not found" (issue #36). A scalar intermediate
|
|
27
|
+
# node (e.g. descending into a String) has no #dig; treat such a path as
|
|
28
|
+
# "not found" rather than crashing. Secret-named keys render masked.
|
|
29
|
+
def self.render_get(key, ui:)
|
|
30
|
+
value =
|
|
31
|
+
begin
|
|
32
|
+
Rubino.configuration.dig(*key.split("."))
|
|
33
|
+
rescue TypeError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
if value.nil?
|
|
37
|
+
ui.warning("Key '#{key}' not found")
|
|
38
|
+
else
|
|
39
|
+
ui.info("#{key} = #{redact(value, key: key.split(".").last)}")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
desc "set KEY VALUE", "Set a configuration value (dot-notation)"
|
|
44
|
+
def set(key, value)
|
|
45
|
+
writer = Config::Writer.new(config_path: config_path)
|
|
46
|
+
writer.set(key, value)
|
|
47
|
+
Rubino.ui.success("#{key} = #{value}")
|
|
48
|
+
rescue ConfigurationError => e
|
|
49
|
+
Rubino.ui.error(e.message)
|
|
50
|
+
exit(1)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "show", "Show full configuration (secrets masked)"
|
|
54
|
+
def show
|
|
55
|
+
self.class.render_show(ui: Rubino.ui)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ONE full-config rendering for both surfaces (#187): this CLI verb and
|
|
59
|
+
# the in-chat `/config show` — with secret-named keys masked, which the
|
|
60
|
+
# clear-text dump never did (api_key landed verbatim in the scrollback).
|
|
61
|
+
def self.render_show(ui:)
|
|
62
|
+
ui.info(redact(Rubino.configuration.raw).to_yaml)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Deep DISPLAY masking for config values (#187): a secret-named key's
|
|
66
|
+
# value renders as *** (Util::SecretsMask — the same heuristic approval
|
|
67
|
+
# prompts use), hashes/arrays are walked, and plain strings are scanned
|
|
68
|
+
# for inline `Bearer …`-style credentials. Display-only — the file and
|
|
69
|
+
# the live configuration keep the real values. Empty/nil values pass
|
|
70
|
+
# through unmasked so a *** never fakes a value that isn't set.
|
|
71
|
+
def self.redact(value, key: nil)
|
|
72
|
+
case value
|
|
73
|
+
when Hash then value.to_h { |k, v| [k, redact(v, key: k)] }
|
|
74
|
+
when Array then value.map { |v| redact(v, key: key) }
|
|
75
|
+
when String
|
|
76
|
+
value.empty? ? value : Util::SecretsMask.mask_value(value, key: key)
|
|
77
|
+
else value
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
desc "path", "Show config file path"
|
|
82
|
+
def path
|
|
83
|
+
Rubino.ui.info(config_path)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Resolve through the Loader so config get/set/path operate on exactly
|
|
89
|
+
# the file the server loads (RUBINO_HOME-aware), not a recomputed
|
|
90
|
+
# File.join off a YAML default.
|
|
91
|
+
def config_path
|
|
92
|
+
Config::Loader.new.config_path
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module CLI
|
|
5
|
+
# Health check command that verifies all system components are working.
|
|
6
|
+
#
|
|
7
|
+
# Doctor is a READ-ONLY diagnosis (#68): it must never create the home
|
|
8
|
+
# directory or the database file while checking them — a never-setup
|
|
9
|
+
# install is reported as "run 'rubino setup'", not silently materialized
|
|
10
|
+
# at the umask's permissions and then declared healthy.
|
|
11
|
+
#
|
|
12
|
+
# Exit status (#67): non-zero when one or more required checks did not
|
|
13
|
+
# pass, so CI/scripts can gate on `rubino doctor`.
|
|
14
|
+
class DoctorCommand
|
|
15
|
+
def execute
|
|
16
|
+
ui = Rubino.ui
|
|
17
|
+
ui.info("Running system diagnostics...")
|
|
18
|
+
ui.blank_line
|
|
19
|
+
|
|
20
|
+
# Required checks score the headline verdict — these are what a CLI user
|
|
21
|
+
# needs for a working install. The encryption key is SERVER-ONLY (JSON
|
|
22
|
+
# API / OAuth) and a CLI-only user never touches it, so it lives in a
|
|
23
|
+
# separate optional section and is NOT counted against the score (#143):
|
|
24
|
+
# a healthy default install reports all-green.
|
|
25
|
+
required = [
|
|
26
|
+
check_config,
|
|
27
|
+
check_database,
|
|
28
|
+
check_migrations,
|
|
29
|
+
check_directories,
|
|
30
|
+
check_provider_keys,
|
|
31
|
+
check_model_configured
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
ui.blank_line
|
|
35
|
+
ui.info("Optional (API/OAuth server):")
|
|
36
|
+
optional = [check_encryption_key]
|
|
37
|
+
|
|
38
|
+
# Document converters are an optional in-process capability (#6): report
|
|
39
|
+
# which CORE formats can be read in-process (their optional gem is
|
|
40
|
+
# loadable), but never let an absent gem fail doctor — pure-ruby formats
|
|
41
|
+
# always work and missing extraction gems only narrow the supported set.
|
|
42
|
+
check_document_converters
|
|
43
|
+
|
|
44
|
+
# MCP servers are optional integrations (#90): report each configured
|
|
45
|
+
# server's reachability best-effort, but never let a down MCP server
|
|
46
|
+
# fail doctor — it is informational, not a required check, so non-MCP
|
|
47
|
+
# users (and MCP users with a flaky server) still exit 0.
|
|
48
|
+
check_mcp_servers if MCP.enabled?
|
|
49
|
+
|
|
50
|
+
ui.blank_line
|
|
51
|
+
passed = required.count { |c| c[:status] == :ok }
|
|
52
|
+
total = required.size
|
|
53
|
+
optional_unconfigured = optional.count { |c| c[:status] != :ok }
|
|
54
|
+
|
|
55
|
+
if passed == total
|
|
56
|
+
ui.success("All #{total} checks passed!")
|
|
57
|
+
if optional_unconfigured.positive?
|
|
58
|
+
ui.info("(#{optional_unconfigured} optional server check#{"s" if optional_unconfigured != 1} not configured — only needed to run the API/OAuth server)")
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
ui.warning("#{passed}/#{total} required checks passed")
|
|
62
|
+
# Scripts/CI gate on doctor: a failed required check must be a
|
|
63
|
+
# non-zero exit, not a green 0 under a red report (#67).
|
|
64
|
+
exit(1)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def check_config
|
|
71
|
+
ui = Rubino.ui
|
|
72
|
+
loader = Config::Loader.new
|
|
73
|
+
|
|
74
|
+
if loader.config_exists?
|
|
75
|
+
ui.success("Config file exists: #{loader.config_path}")
|
|
76
|
+
{ name: "config", status: :ok }
|
|
77
|
+
else
|
|
78
|
+
ui.error("config file missing. Run 'rubino setup'")
|
|
79
|
+
{ name: "config", status: :fail }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def check_database
|
|
84
|
+
ui = Rubino.ui
|
|
85
|
+
unless database_on_disk?
|
|
86
|
+
ui.error("database not initialized: #{Rubino.database.db_path}. Run 'rubino setup'")
|
|
87
|
+
return { name: "database", status: :fail }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if Rubino.database.healthy?
|
|
91
|
+
ui.success("Database accessible: #{Rubino.database.db_path}")
|
|
92
|
+
{ name: "database", status: :ok }
|
|
93
|
+
else
|
|
94
|
+
ui.error("database not accessible")
|
|
95
|
+
{ name: "database", status: :fail }
|
|
96
|
+
end
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
ui.error("database error: #{e.message}")
|
|
99
|
+
{ name: "database", status: :fail }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def check_migrations
|
|
103
|
+
ui = Rubino.ui
|
|
104
|
+
unless database_on_disk?
|
|
105
|
+
ui.error("migrations not run — no database. Run 'rubino setup'")
|
|
106
|
+
return { name: "migrations", status: :fail }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
migrator = Database::Migrator.new(Rubino.database)
|
|
110
|
+
|
|
111
|
+
if migrator.pending?
|
|
112
|
+
ui.warning("Pending migrations exist")
|
|
113
|
+
{ name: "migrations", status: :warn }
|
|
114
|
+
else
|
|
115
|
+
ui.success("Migrations up to date")
|
|
116
|
+
{ name: "migrations", status: :ok }
|
|
117
|
+
end
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
ui.error("migration check failed: #{e.message}")
|
|
120
|
+
{ name: "migrations", status: :fail }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Read-only guard for the two DB checks (#68): SQLite lazily CREATES the
|
|
124
|
+
# file (and its parent directory) on the first connection, so probing a
|
|
125
|
+
# never-setup home with `SELECT 1` would mutate it — and doctor would then
|
|
126
|
+
# report the empty, unmigrated database it just created as "accessible".
|
|
127
|
+
# A missing file is an uninitialized install: report it without touching
|
|
128
|
+
# the disk.
|
|
129
|
+
def database_on_disk?
|
|
130
|
+
db = Rubino.database
|
|
131
|
+
db.memory? || File.exist?(db.db_path)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def check_directories
|
|
135
|
+
ui = Rubino.ui
|
|
136
|
+
home = Rubino.home_path
|
|
137
|
+
|
|
138
|
+
if File.directory?(home)
|
|
139
|
+
ui.success("Home directory exists: #{home}")
|
|
140
|
+
{ name: "directories", status: :ok }
|
|
141
|
+
else
|
|
142
|
+
ui.error("home directory missing: #{home}. Run 'rubino setup'")
|
|
143
|
+
{ name: "directories", status: :fail }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Verifies the credentials for the ACTUALLY configured provider resolve —
|
|
148
|
+
# not a hardcoded ENV allowlist. A tenant on an openai_compatible backend
|
|
149
|
+
# (ollama, vllm, a hosted gateway, …) configures its key under
|
|
150
|
+
# providers.<name>.api_key in config.yml; the old hardcoded check ignored
|
|
151
|
+
# that and warned "No API keys found" on a correctly-configured tenant.
|
|
152
|
+
def check_provider_keys
|
|
153
|
+
ui = Rubino.ui
|
|
154
|
+
provider = LLM::CredentialCheck.resolved_provider
|
|
155
|
+
|
|
156
|
+
if LLM::CredentialCheck.usable?
|
|
157
|
+
ui.success("API key configured (#{provider})")
|
|
158
|
+
{ name: "provider_keys", status: :ok }
|
|
159
|
+
else
|
|
160
|
+
ui.warning("No credentials found for provider '#{provider}'")
|
|
161
|
+
{ name: "provider_keys", status: :warn }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def check_model_configured
|
|
166
|
+
ui = Rubino.ui
|
|
167
|
+
model = Rubino.configuration.model_default
|
|
168
|
+
|
|
169
|
+
if model && !model.empty?
|
|
170
|
+
ui.success("Model configured: #{model}")
|
|
171
|
+
{ name: "model", status: :ok }
|
|
172
|
+
else
|
|
173
|
+
ui.error("no model configured")
|
|
174
|
+
{ name: "model", status: :fail }
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Verifies the OAuth-token encryption key is present and well-formed
|
|
179
|
+
# WITHOUT crashing doctor itself: server boot uses Boot::EncryptionKey
|
|
180
|
+
# for the hard fail-fast path, but doctor must keep running so the
|
|
181
|
+
# operator sees every other check that did pass.
|
|
182
|
+
#
|
|
183
|
+
# The key is only needed by the JSON API / OAuth (encrypted-token) path;
|
|
184
|
+
# a CLI-only user never touches it. So a MISSING key is a :warn scoped to
|
|
185
|
+
# that path, not a scary red :fail that makes a healthy CLI install look
|
|
186
|
+
# broken (F4). A key that IS set but malformed is still a real :fail —
|
|
187
|
+
# that's a misconfiguration the operator must fix before the server boots.
|
|
188
|
+
def check_encryption_key
|
|
189
|
+
ui = Rubino.ui
|
|
190
|
+
OAuth::TokenEncryptor.from_env
|
|
191
|
+
ui.success("Encryption key configured")
|
|
192
|
+
{ name: "encryption_key", status: :ok }
|
|
193
|
+
rescue OAuth::TokenEncryptor::KeyMissingError
|
|
194
|
+
ui.warning("RUBINO_ENCRYPTION_KEY not set (only needed for the API/OAuth server)")
|
|
195
|
+
{ name: "encryption_key", status: :warn }
|
|
196
|
+
rescue ArgumentError => e
|
|
197
|
+
ui.error("RUBINO_ENCRYPTION_KEY invalid: #{e.message}")
|
|
198
|
+
{ name: "encryption_key", status: :fail }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Best-effort MCP reachability report (#90). Starts each configured
|
|
202
|
+
# server, health-checks it, and stops everything again — doctor stays
|
|
203
|
+
# read-only and leaves no child processes behind. Deliberately NOT part
|
|
204
|
+
# of the required score: a server that fails to start already warned via
|
|
205
|
+
# Manager#start_server, a started-but-dead one warns here, and neither
|
|
206
|
+
# flips the exit status. Any unexpected error degrades to a warning so
|
|
207
|
+
# the MCP section can never break doctor itself.
|
|
208
|
+
def check_mcp_servers
|
|
209
|
+
ui = Rubino.ui
|
|
210
|
+
ui.blank_line
|
|
211
|
+
ui.info("Optional (MCP servers, experimental):")
|
|
212
|
+
|
|
213
|
+
servers = Rubino.configuration.dig("mcp", "servers") || {}
|
|
214
|
+
manager = MCP::Manager.new
|
|
215
|
+
servers.each { |name, server_config| manager.start_server(name, server_config) }
|
|
216
|
+
|
|
217
|
+
manager.health_check.each do |status|
|
|
218
|
+
if status[:alive]
|
|
219
|
+
ui.success("MCP server '#{status[:name]}' reachable")
|
|
220
|
+
else
|
|
221
|
+
ui.warning("MCP server '#{status[:name]}' not reachable")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
manager.stop_all!
|
|
225
|
+
rescue StandardError => e
|
|
226
|
+
ui.warning("MCP check failed: #{e.message}")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Non-scoring report of the in-process document-conversion capability
|
|
230
|
+
# (#6), mirroring the MCP "Optional (…)" pattern. Pure-ruby formats are
|
|
231
|
+
# always green; a gem-backed format whose optional gem isn't installed is
|
|
232
|
+
# a warning (never a fail), so a healthy default install never shows red
|
|
233
|
+
# for a capability it can extend by installing an optional gem.
|
|
234
|
+
def check_document_converters
|
|
235
|
+
ui = Rubino.ui
|
|
236
|
+
ui.blank_line
|
|
237
|
+
ui.info("Optional (document converters, in-process via read_attachment):")
|
|
238
|
+
|
|
239
|
+
Rubino::Documents::Registry.capabilities.each do |format, available|
|
|
240
|
+
if available
|
|
241
|
+
ui.success("#{format} supported")
|
|
242
|
+
else
|
|
243
|
+
ui.warning("#{format} not available (install its optional gem to enable)")
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
rescue StandardError => e
|
|
247
|
+
ui.warning("Document-converter check failed: #{e.message}")
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module CLI
|
|
7
|
+
# Subcommands for managing the job queue
|
|
8
|
+
class JobsCommand < Thor
|
|
9
|
+
# Clean `tree`/help label instead of the underscored class-name default (F12).
|
|
10
|
+
namespace "rubino jobs"
|
|
11
|
+
|
|
12
|
+
def self.exit_on_failure?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "list", "List jobs in queue"
|
|
17
|
+
option :status, type: :string, desc: "Filter by status (queued, running, completed, failed)"
|
|
18
|
+
option :limit, type: :numeric, default: 20, desc: "Max results"
|
|
19
|
+
def list
|
|
20
|
+
Rubino.ensure_database_ready!
|
|
21
|
+
queue = Jobs::Queue.new
|
|
22
|
+
jobs = queue.list(status: options[:status], limit: options[:limit])
|
|
23
|
+
|
|
24
|
+
if jobs.empty?
|
|
25
|
+
Rubino.ui.info("No jobs found.")
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
self.class.render_list(jobs, ui: Rubino.ui)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ONE jobs-table rendering for both surfaces (#187): this CLI verb and
|
|
33
|
+
# the in-chat /jobs list (Commands::Executor).
|
|
34
|
+
def self.render_list(jobs, ui:)
|
|
35
|
+
rows = jobs.map do |j|
|
|
36
|
+
[j[:id][0..7], j[:type], j[:status], j[:attempts].to_s, j[:run_at]]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
ui.table(headers: %w[ID Type Status Attempts RunAt], rows: rows)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "process", "Run pending jobs now (manual mode)"
|
|
43
|
+
option :limit, type: :numeric, default: 10, desc: "Max jobs to process"
|
|
44
|
+
def process
|
|
45
|
+
runner = Jobs::Runner.new
|
|
46
|
+
processed = runner.run_pending(limit: options[:limit])
|
|
47
|
+
Rubino.ui.success("Processed #{processed} job(s)")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
desc "worker", "Start a background worker loop"
|
|
51
|
+
def worker
|
|
52
|
+
Rubino.ui.info("Starting job worker (poll every #{Rubino.configuration.jobs_poll_interval}s)...")
|
|
53
|
+
Rubino.ui.info("Press Ctrl+C to stop.")
|
|
54
|
+
|
|
55
|
+
worker = Jobs::Worker.new
|
|
56
|
+
worker.start
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|