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,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module CLI
|
|
7
|
+
# Subcommands for managing persistent memories
|
|
8
|
+
class MemoryCommand < Thor
|
|
9
|
+
# Clean `tree`/help label instead of the underscored class-name default (F12).
|
|
10
|
+
namespace "rubino memory"
|
|
11
|
+
|
|
12
|
+
def self.exit_on_failure?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "list", "List stored memories (live facts only; --all includes superseded)"
|
|
17
|
+
option :kind, type: :string, desc: "Filter by memory kind"
|
|
18
|
+
option :limit, type: :numeric, default: 20, desc: "Max results"
|
|
19
|
+
option :all, type: :boolean, default: false,
|
|
20
|
+
desc: "Include superseded (soft-retired) facts"
|
|
21
|
+
def list
|
|
22
|
+
Rubino.ensure_database_ready!
|
|
23
|
+
memories = backend_store.list(kind: options[:kind], limit: options[:limit],
|
|
24
|
+
include_retired: options[:all])
|
|
25
|
+
|
|
26
|
+
if memories.empty?
|
|
27
|
+
Rubino.ui.info("No memories found.")
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
rows = memories.map do |m|
|
|
32
|
+
[m[:id][0..7], m[:kind], "#{m[:content][0..60]}#{self.class.retired_marker(m)}", m[:created_at]]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Rubino.ui.table(
|
|
36
|
+
headers: %w[ID Kind Content Created],
|
|
37
|
+
rows: rows
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "show ID", "Show a specific memory"
|
|
42
|
+
def show(id)
|
|
43
|
+
memory = backend_store.find(id)
|
|
44
|
+
|
|
45
|
+
if memory.nil?
|
|
46
|
+
Rubino.ui.error("memory not found: #{id}")
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
self.class.render(memory, ui: Rubino.ui)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ONE fact-details rendering for both surfaces (#184): the CLI verb
|
|
54
|
+
# above and the in-chat `/memory show <id>` (Commands::Executor).
|
|
55
|
+
def self.render(memory, ui:)
|
|
56
|
+
ui.info("ID: #{memory[:id]}")
|
|
57
|
+
ui.info("Kind: #{memory[:kind]}")
|
|
58
|
+
ui.info("Confidence: #{memory[:confidence]}")
|
|
59
|
+
ui.info("Created: #{memory[:created_at]}")
|
|
60
|
+
# The temporal chain (#88): a soft-retired fact shows when it stopped
|
|
61
|
+
# being true and which fact replaced it.
|
|
62
|
+
if memory[:valid_to]
|
|
63
|
+
ui.info("Retired: #{memory[:valid_to]}")
|
|
64
|
+
ui.info("Superseded by: #{memory[:superseded_by]}") if memory[:superseded_by]
|
|
65
|
+
end
|
|
66
|
+
ui.separator
|
|
67
|
+
ui.info(memory[:content])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
desc "delete ID", "Delete a specific memory"
|
|
71
|
+
def delete(id)
|
|
72
|
+
if backend_store.delete(id)
|
|
73
|
+
Rubino.ui.success("Memory deleted: #{id}")
|
|
74
|
+
else
|
|
75
|
+
Rubino.ui.error("memory not found: #{id}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
desc "backend [NAME]", "Show the active memory backend, or switch to NAME"
|
|
80
|
+
def backend(name = nil)
|
|
81
|
+
return show_backend if name.nil?
|
|
82
|
+
|
|
83
|
+
unless Memory::Backends.registered?(name)
|
|
84
|
+
Rubino.ui.error(
|
|
85
|
+
"Unknown memory backend: #{name}. Available: #{Memory::Backends.names.join(", ")}"
|
|
86
|
+
)
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
Config::Writer.new(config_path: config_path).set("memory.backend", name)
|
|
91
|
+
Rubino.ui.success("memory.backend = #{name}")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# `--all` surfaces soft-retired rows next to live ones; without a flag
|
|
95
|
+
# they were indistinguishable and the supersession chain needed a `show`
|
|
96
|
+
# per id (#161). Marks a tombstone with its retirement date and, when
|
|
97
|
+
# known, the short id of the fact that replaced it. A class method so the
|
|
98
|
+
# in-chat `/memory --all` table (#184) speaks the same dialect.
|
|
99
|
+
def self.retired_marker(memory)
|
|
100
|
+
return "" unless memory[:valid_to]
|
|
101
|
+
|
|
102
|
+
marker = " (retired #{memory[:valid_to][0..9]}"
|
|
103
|
+
marker += " → #{memory[:superseded_by][0..7]}" if memory[:superseded_by]
|
|
104
|
+
"#{marker})"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# ONE backend summary for both surfaces (#184): the CLI `memory backend`
|
|
108
|
+
# verb and the in-chat `/memory backend`.
|
|
109
|
+
def self.render_active_backend(ui:)
|
|
110
|
+
active = Rubino.configuration.dig("memory", "backend") || Memory::Backends::DEFAULT_NAME
|
|
111
|
+
ui.info("Active backend: #{active}")
|
|
112
|
+
ui.info("Available: #{Memory::Backends.names.join(", ")}")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# Resolve the *configured* memory backend (default: sqlite tiny-Zep), the
|
|
118
|
+
# same store the agent loop, the in-chat `/memory` view and the HTTP
|
|
119
|
+
# `/v1/memory` ops use. The old `Memory::Store.new` was hardwired to the
|
|
120
|
+
# legacy `:memories` table and ignored `memory.backend`, so list/show/delete
|
|
121
|
+
# never saw the facts the agent actually persists (#94).
|
|
122
|
+
def backend_store
|
|
123
|
+
@backend_store ||= Memory::Backends.build
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def show_backend
|
|
127
|
+
self.class.render_active_backend(ui: Rubino.ui)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def config_path
|
|
131
|
+
Config::Loader.new.config_path
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module CLI
|
|
7
|
+
# First-run onboarding (#93). A small, skippable interactive wizard that
|
|
8
|
+
# takes a brand-new user from an empty home to a working model: pick a
|
|
9
|
+
# provider/model, paste the key (written to .env, never echoed back), and
|
|
10
|
+
# persist the matching model.default / model.provider / providers.<name>
|
|
11
|
+
# block to config.yml. The catalog mirrors DOCS-BLUEPRINT models-and-keys.
|
|
12
|
+
#
|
|
13
|
+
# It is only invoked when no usable credential is configured AND we are on a
|
|
14
|
+
# real TTY (ChatCommand#ensure_model_configured!); non-interactive contexts
|
|
15
|
+
# get the actionable guidance instead. #run returns true on a completed
|
|
16
|
+
# setup, false if the user skipped — the caller re-checks usability either
|
|
17
|
+
# way, so a partial/declined run safely falls through to the guidance+exit.
|
|
18
|
+
class OnboardingWizard
|
|
19
|
+
# Each provider: the model.provider to write, a default model id, the .env
|
|
20
|
+
# key var, and any providers.<name> config block to persist. Ordered so the
|
|
21
|
+
# recommended default comes first and matches the seeded config default
|
|
22
|
+
# (config/defaults.rb model.default => openai/gpt-4.1), keeping the from-zero
|
|
23
|
+
# experience consistent between the wizard and the non-interactive
|
|
24
|
+
# fail-fast guidance.
|
|
25
|
+
PROVIDERS = [
|
|
26
|
+
{
|
|
27
|
+
key: "openai",
|
|
28
|
+
label: "OpenAI (GPT) — recommended default",
|
|
29
|
+
provider: "openai",
|
|
30
|
+
model: "gpt-4.1",
|
|
31
|
+
env_var: "OPENAI_API_KEY",
|
|
32
|
+
config: {}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "minimax",
|
|
36
|
+
label: "MiniMax (Anthropic-compatible)",
|
|
37
|
+
provider: "minimax",
|
|
38
|
+
model: "MiniMax-M2.7",
|
|
39
|
+
env_var: "MINIMAX_API_KEY",
|
|
40
|
+
config: {
|
|
41
|
+
"anthropic_compatible" => true,
|
|
42
|
+
"base_url" => "https://api.minimax.io/anthropic",
|
|
43
|
+
"api_key" => "${MINIMAX_API_KEY}"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "anthropic",
|
|
48
|
+
label: "Anthropic (Claude)",
|
|
49
|
+
provider: "anthropic",
|
|
50
|
+
model: "claude-sonnet-4-5",
|
|
51
|
+
env_var: "ANTHROPIC_API_KEY",
|
|
52
|
+
config: {}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "gemini",
|
|
56
|
+
label: "Google (Gemini)",
|
|
57
|
+
provider: "google",
|
|
58
|
+
model: "gemini-2.5-pro",
|
|
59
|
+
env_var: "GEMINI_API_KEY",
|
|
60
|
+
config: {}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "gateway",
|
|
64
|
+
label: "OpenAI-compatible gateway",
|
|
65
|
+
provider: "gateway",
|
|
66
|
+
model: "auto",
|
|
67
|
+
env_var: "OPENAI_API_KEY",
|
|
68
|
+
config: {
|
|
69
|
+
"openai_compatible" => true,
|
|
70
|
+
"assume_model_exists" => true,
|
|
71
|
+
"base_url" => nil # filled in interactively
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
].freeze
|
|
75
|
+
|
|
76
|
+
def initialize(ui: Rubino.ui, input: $stdin, output: $stdout)
|
|
77
|
+
@ui = ui
|
|
78
|
+
@input = input
|
|
79
|
+
@output = output
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Drives the wizard. Returns true when a provider was configured, false
|
|
83
|
+
# when the user skipped (empty/`s`/`skip` at the provider prompt).
|
|
84
|
+
def run
|
|
85
|
+
@ui.blank_line
|
|
86
|
+
@ui.info("Welcome to rubino — let's get you connected to a model.")
|
|
87
|
+
@ui.status("No API key is configured yet. Pick a provider (or press Enter to skip).")
|
|
88
|
+
@ui.blank_line
|
|
89
|
+
|
|
90
|
+
choice = ask_provider
|
|
91
|
+
return false unless choice
|
|
92
|
+
|
|
93
|
+
api_key = ask_api_key(choice)
|
|
94
|
+
return false if api_key.nil? || api_key.empty?
|
|
95
|
+
|
|
96
|
+
base_url = ask_base_url(choice)
|
|
97
|
+
|
|
98
|
+
persist!(choice, api_key, base_url)
|
|
99
|
+
Rubino.reload_configuration!
|
|
100
|
+
|
|
101
|
+
@ui.blank_line
|
|
102
|
+
@ui.success("Configured #{choice[:label]} with model #{choice[:model]}.")
|
|
103
|
+
@ui.status("Saved to #{config_loader.config_path} and #{config_loader.env_path}.")
|
|
104
|
+
@ui.blank_line
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def ask_provider
|
|
111
|
+
PROVIDERS.each_with_index do |p, i|
|
|
112
|
+
@output.puts " #{i + 1}) #{p[:label]}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Re-prompt on an invalid choice instead of abandoning the wizard on
|
|
116
|
+
# the first typo (#31). Only an explicit skip (empty / `s` / `skip`) or
|
|
117
|
+
# EOF leaves the loop with nil; an out-of-range number just asks again.
|
|
118
|
+
loop do
|
|
119
|
+
@output.print "Choose a provider [1-#{PROVIDERS.size}, Enter to skip]: "
|
|
120
|
+
@output.flush
|
|
121
|
+
raw = read_line
|
|
122
|
+
return nil if raw.nil? || raw.strip.empty? || %w[s skip].include?(raw.strip.downcase)
|
|
123
|
+
|
|
124
|
+
idx = raw.strip.to_i
|
|
125
|
+
return PROVIDERS[idx - 1] if idx.between?(1, PROVIDERS.size)
|
|
126
|
+
|
|
127
|
+
@ui.warning("Not a valid choice — please pick 1-#{PROVIDERS.size}, or press Enter to skip.")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def ask_api_key(choice)
|
|
132
|
+
@output.print "Paste your #{choice[:env_var]} (input hidden; Enter to skip): "
|
|
133
|
+
@output.flush
|
|
134
|
+
read_secret.to_s.strip
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The proxy provider needs a base_url; everyone else uses the upstream
|
|
138
|
+
# default, so we only ask when the catalog entry left base_url nil.
|
|
139
|
+
def ask_base_url(choice)
|
|
140
|
+
return nil unless choice[:config].key?("base_url") && choice[:config]["base_url"].nil?
|
|
141
|
+
|
|
142
|
+
@output.print "Enter the gateway base URL (e.g. https://host/v1): "
|
|
143
|
+
@output.flush
|
|
144
|
+
read_line.to_s.strip
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def persist!(choice, api_key, base_url)
|
|
148
|
+
Rubino.ensure_directories!
|
|
149
|
+
loader = config_loader
|
|
150
|
+
# Seed config.yml from defaults the first time so the wizard's keys land
|
|
151
|
+
# in a complete, hand-editable file rather than a 3-line stub.
|
|
152
|
+
loader.create_default_config! unless loader.config_exists?
|
|
153
|
+
|
|
154
|
+
writer = Config::Writer.new(config_path: loader.config_path)
|
|
155
|
+
writer.set("model.default", choice[:model])
|
|
156
|
+
writer.set("model.provider", choice[:provider])
|
|
157
|
+
|
|
158
|
+
choice[:config].each do |k, v|
|
|
159
|
+
value = k == "base_url" && (v.nil? || v.empty?) ? base_url : v
|
|
160
|
+
next if value.nil?
|
|
161
|
+
|
|
162
|
+
writer.set("providers.#{choice[:provider]}.#{k}", value)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
write_env_key!(loader.env_path, choice[:env_var], api_key)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Appends/updates KEY=value in .env (0600). Does not echo the value. An
|
|
169
|
+
# existing line for the same key is replaced so re-running setup updates it.
|
|
170
|
+
def write_env_key!(env_path, var, value)
|
|
171
|
+
lines = File.exist?(env_path) ? File.readlines(env_path, chomp: true) : []
|
|
172
|
+
lines.reject! { |l| l =~ /\A#{Regexp.escape(var)}=/ }
|
|
173
|
+
lines << "#{var}=#{value}"
|
|
174
|
+
File.write(env_path, lines.join("\n") + "\n")
|
|
175
|
+
File.chmod(0o600, env_path)
|
|
176
|
+
# Make the key visible to THIS process too, so the immediate usability
|
|
177
|
+
# re-check and any subsequent model call in this run can see it.
|
|
178
|
+
ENV[var] = value
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def config_loader
|
|
182
|
+
@config_loader ||= Config::Loader.new
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def read_line
|
|
186
|
+
@input.gets
|
|
187
|
+
rescue StandardError
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Hidden input for the key. Falls back to a plain read when the terminal
|
|
192
|
+
# can't toggle echo (piped input in tests).
|
|
193
|
+
def read_secret
|
|
194
|
+
if @input.respond_to?(:noecho) && @input.tty?
|
|
195
|
+
begin
|
|
196
|
+
secret = @input.noecho(&:gets)
|
|
197
|
+
@output.puts
|
|
198
|
+
return secret
|
|
199
|
+
rescue StandardError
|
|
200
|
+
# fall through to plain read
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
read_line
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module CLI
|
|
5
|
+
# Starts the HTTP API server (Rack + Puma).
|
|
6
|
+
class ServerCommand
|
|
7
|
+
def initialize(options = {})
|
|
8
|
+
@options = options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute
|
|
12
|
+
# Fail fast: a missing/malformed encryption key blows up on the first
|
|
13
|
+
# OAuth hit otherwise, with the listener already accepting traffic.
|
|
14
|
+
Boot::EncryptionKey.validate!
|
|
15
|
+
|
|
16
|
+
# The fake LLM provider is dev-only — it replays canned YAML scenarios
|
|
17
|
+
# instead of talking to a real LLM, so booting the API with it on by
|
|
18
|
+
# accident would silently serve fake answers to real clients. Refuse
|
|
19
|
+
# to start unless the operator explicitly opted in.
|
|
20
|
+
guard_fake_provider!
|
|
21
|
+
|
|
22
|
+
port = (@options[:port] || ENV.fetch("RUBINO_API_PORT", 4820)).to_i
|
|
23
|
+
# Loopback by default (#69); a routable bind is an explicit opt-in.
|
|
24
|
+
host = @options[:host] || ENV.fetch("RUBINO_API_HOST", "127.0.0.1")
|
|
25
|
+
api_key = @options[:api_key] || ENV.fetch("RUBINO_API_KEY", nil)
|
|
26
|
+
|
|
27
|
+
# When TLS is enabled (RUBINO_TLS=1 or a cert already exists), make
|
|
28
|
+
# sure a self-signed cert+key exist under RUBINO_HOME and serve over
|
|
29
|
+
# HTTPS. The web client pins this cert. Local dev / fake leave the
|
|
30
|
+
# toggle unset and no cert, so the listener stays plain HTTP.
|
|
31
|
+
tls_cert = tls_key = nil
|
|
32
|
+
if API::TLS.enabled?
|
|
33
|
+
API::TLS.ensure_cert!(host: host)
|
|
34
|
+
tls_cert = API::TLS.cert_path
|
|
35
|
+
tls_key = API::TLS.key_path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
register_metric_descriptions!
|
|
39
|
+
|
|
40
|
+
# Without this the tool registry stays empty, Lifecycle#load_tools
|
|
41
|
+
# returns [], no `tools: [...]` is sent on the wire, and the model
|
|
42
|
+
# has no choice but to roleplay tools in markdown. The CLI path
|
|
43
|
+
# (ChatCommand#ensure_setup!) registers tools the same way; both
|
|
44
|
+
# entry points need the same line.
|
|
45
|
+
Rubino::Tools::Registry.register_defaults! if Rubino::Tools::Registry.all.empty?
|
|
46
|
+
|
|
47
|
+
# Instantiate the shared agent registry at boot so the `task` tool can
|
|
48
|
+
# resolve subagents (explore/general) over /v1 — the API path uses the
|
|
49
|
+
# same delegation flow as the CLI. Memoized on Rubino.agent_registry.
|
|
50
|
+
Rubino.agent_registry
|
|
51
|
+
|
|
52
|
+
router = API::Router.new
|
|
53
|
+
router.get "/v1/health", to: API::Operations::HealthOperation
|
|
54
|
+
router.get "/v1/metrics", to: API::Operations::MetricsOperation
|
|
55
|
+
router.get "/v1/sessions", to: API::Operations::Sessions::IndexOperation
|
|
56
|
+
router.post "/v1/sessions", to: API::Operations::Sessions::CreateOperation
|
|
57
|
+
router.get "/v1/sessions/:id", to: API::Operations::Sessions::ShowOperation
|
|
58
|
+
router.delete "/v1/sessions/:id", to: API::Operations::Sessions::DeleteOperation
|
|
59
|
+
router.post "/v1/sessions/:id/runs", to: API::Operations::Runs::CreateOperation
|
|
60
|
+
router.get "/v1/runs/:id/events", to: API::Operations::Runs::EventsOperation
|
|
61
|
+
router.post "/v1/runs/:id/stop", to: API::Operations::Runs::StopOperation
|
|
62
|
+
router.post "/v1/sessions/:id/retry", to: API::Operations::Sessions::RetryOperation
|
|
63
|
+
router.post "/v1/sessions/:id/undo", to: API::Operations::Sessions::UndoOperation
|
|
64
|
+
router.post "/v1/runs/:run_id/approvals/:approval_id", to: API::Operations::Approvals::DecideOperation
|
|
65
|
+
router.post "/v1/runs/:run_id/clarifications/:clarify_id", to: API::Operations::Clarifications::DecideOperation
|
|
66
|
+
router.get "/v1/skills", to: API::Operations::Skills::ListOperation
|
|
67
|
+
router.put "/v1/skills/:name", to: API::Operations::Skills::ToggleOperation
|
|
68
|
+
router.get "/v1/mode", to: API::Operations::Mode::ShowOperation
|
|
69
|
+
router.put "/v1/mode", to: API::Operations::Mode::UpdateOperation
|
|
70
|
+
router.get "/v1/models", to: API::Operations::Models::ListOperation
|
|
71
|
+
router.get "/v1/files", to: API::Operations::Files::ReadOperation
|
|
72
|
+
router.post "/v1/files", to: API::Operations::Files::UploadOperation
|
|
73
|
+
router.get "/v1/jobs", to: API::Operations::CronJobs::ListOperation
|
|
74
|
+
router.post "/v1/jobs", to: API::Operations::CronJobs::CreateOperation
|
|
75
|
+
router.get "/v1/jobs/:id", to: API::Operations::CronJobs::ShowOperation
|
|
76
|
+
router.patch "/v1/jobs/:id", to: API::Operations::CronJobs::UpdateOperation
|
|
77
|
+
router.delete "/v1/jobs/:id", to: API::Operations::CronJobs::DeleteOperation
|
|
78
|
+
router.post "/v1/jobs/:id/pause", to: API::Operations::CronJobs::PauseOperation
|
|
79
|
+
router.post "/v1/jobs/:id/resume", to: API::Operations::CronJobs::ResumeOperation
|
|
80
|
+
router.post "/v1/jobs/:id/trigger", to: API::Operations::CronJobs::TriggerOperation
|
|
81
|
+
router.get "/v1/memory", to: API::Operations::Memory::IndexOperation
|
|
82
|
+
router.get "/v1/memory/stats", to: API::Operations::Memory::StatsOperation
|
|
83
|
+
router.delete "/v1/memory/:id", to: API::Operations::Memory::DeleteOperation
|
|
84
|
+
router.get "/v1/tasks", to: API::Operations::Tasks::IndexOperation
|
|
85
|
+
router.get "/v1/tasks/:id", to: API::Operations::Tasks::ShowOperation
|
|
86
|
+
router.post "/v1/tasks/:id/stop", to: API::Operations::Tasks::StopOperation
|
|
87
|
+
router.get "/v1/oauth/providers", to: API::Operations::OAuth::Providers::ListOperation
|
|
88
|
+
router.post "/v1/oauth/providers/:id/connect", to: API::Operations::OAuth::Providers::ConnectOperation
|
|
89
|
+
router.post "/v1/oauth/providers/:id/callback", to: API::Operations::OAuth::Providers::CallbackOperation
|
|
90
|
+
router.get "/v1/oauth/connections", to: API::Operations::OAuth::Connections::ListOperation
|
|
91
|
+
router.delete "/v1/oauth/connections/:id", to: API::Operations::OAuth::Connections::DisconnectOperation
|
|
92
|
+
|
|
93
|
+
::Rubino::OAuth::Registry.load_from_config!
|
|
94
|
+
Jobs::Scheduler.instance.load_all!
|
|
95
|
+
# Drains any webhook delivery that was persisted as pending before a
|
|
96
|
+
# prior crash/restart. See Jobs::WebhookDelivery#resume_pending!.
|
|
97
|
+
Jobs::Scheduler.instance.resume_pending_webhooks!
|
|
98
|
+
|
|
99
|
+
Rubino::API::Server.new(
|
|
100
|
+
port: port,
|
|
101
|
+
host: host,
|
|
102
|
+
api_key: api_key,
|
|
103
|
+
router: router,
|
|
104
|
+
tls_cert: tls_cert,
|
|
105
|
+
tls_key: tls_key
|
|
106
|
+
).start!
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def guard_fake_provider!
|
|
112
|
+
provider = Rubino.configuration.model_provider
|
|
113
|
+
return unless provider.to_s == "fake"
|
|
114
|
+
return if ENV["RUBINO_ALLOW_FAKE"] == "1"
|
|
115
|
+
|
|
116
|
+
warn "fake provider is dev-only — set RUBINO_ALLOW_FAKE=1 to opt in."
|
|
117
|
+
exit(1)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# HELP text is looked up by Metrics.counter/.histogram at first-touch, so
|
|
121
|
+
# this must run before any metric is incremented (i.e. before the Rack
|
|
122
|
+
# stack is built). When adding a new Metrics.counter/.histogram anywhere
|
|
123
|
+
# in the codebase, add its HELP line here — the metrics_help_spec asserts
|
|
124
|
+
# every registered metric carries a description.
|
|
125
|
+
def register_metric_descriptions!
|
|
126
|
+
Metrics.describe(:http_requests_total, "Total HTTP requests handled, labelled by method/path/status.")
|
|
127
|
+
Metrics.describe(:http_request_duration_seconds, "HTTP request duration in seconds.")
|
|
128
|
+
Metrics.describe(:cron_fires_total, "Number of cron jobs fired, labelled by job and outcome.")
|
|
129
|
+
Metrics.describe(:webhook_deliveries_total, "Webhook deliveries attempted, labelled by outcome.")
|
|
130
|
+
Metrics.describe(:oauth_token_exchanges_total, "OAuth token exchanges, labelled by provider and outcome.")
|
|
131
|
+
Metrics.describe(:runs_total, "Runs started, labelled by source.")
|
|
132
|
+
Metrics.describe(:runs_completed_total, "Total number of runs that have completed (success+failure+cancelled).")
|
|
133
|
+
Metrics.describe(:skills_loaded_total, "Number of times a skill was successfully loaded via the `skill` tool.")
|
|
134
|
+
Metrics.describe(:skills_created_total,
|
|
135
|
+
"Number of new skills observed by the registry on a re-scan (disk-diff signal; no creation tool exists).")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module CLI
|
|
7
|
+
# Subcommands for managing chat sessions
|
|
8
|
+
class SessionCommand < Thor
|
|
9
|
+
# Clean `tree`/help label instead of the underscored class-name default (F12).
|
|
10
|
+
namespace "rubino sessions"
|
|
11
|
+
|
|
12
|
+
def self.exit_on_failure?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "list", "List recent sessions"
|
|
17
|
+
option :limit, type: :numeric, default: 20, desc: "Max results"
|
|
18
|
+
option :status, type: :string, desc: "Filter by status"
|
|
19
|
+
option :search, type: :string, desc: "Filter by title (substring match)"
|
|
20
|
+
def list
|
|
21
|
+
Rubino.ensure_database_ready!
|
|
22
|
+
repo = Session::Repository.new
|
|
23
|
+
# Reap sessions left "active" by a process that died without ending them
|
|
24
|
+
# (hard terminal kill / SIGKILL, #11) so the list never shows a stale
|
|
25
|
+
# "active" for a window that is actually gone.
|
|
26
|
+
repo.reap_orphaned_active!
|
|
27
|
+
sessions = repo.list(limit: options[:limit], status: options[:status],
|
|
28
|
+
search: options[:search])
|
|
29
|
+
|
|
30
|
+
if sessions.empty?
|
|
31
|
+
Rubino.ui.info("No sessions found.")
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
rows = sessions.map do |s|
|
|
36
|
+
[s[:id][0..7], s[:title] || "(untitled)", s[:status],
|
|
37
|
+
s[:message_count].to_s, s[:created_at]]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Rubino.ui.table(
|
|
41
|
+
headers: %w[ID Title Status Messages Created],
|
|
42
|
+
rows: rows
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "show ID", "Show session details"
|
|
47
|
+
def show(id)
|
|
48
|
+
Rubino.ensure_database_ready!
|
|
49
|
+
repo = Session::Repository.new
|
|
50
|
+
session = repo.find(id)
|
|
51
|
+
|
|
52
|
+
# One error, one style (#20): Thor already prints the Thor::Error message
|
|
53
|
+
# to stderr and exits non-zero (exit_on_failure?), so the extra styled
|
|
54
|
+
# ui.error line was the same failure repeated in a second format.
|
|
55
|
+
raise Thor::Error, "session not found: #{id}" if session.nil?
|
|
56
|
+
|
|
57
|
+
self.class.render(session, ui: Rubino.ui)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# ONE session-details rendering for both surfaces (#183): the CLI verb
|
|
61
|
+
# above and the in-chat `/sessions show <id>` (Commands::Executor).
|
|
62
|
+
def self.render(session, ui:)
|
|
63
|
+
ui.info("Session: #{session[:id]}")
|
|
64
|
+
ui.info("Title: #{session[:title] || "(untitled)"}")
|
|
65
|
+
ui.info("Status: #{session[:status]}")
|
|
66
|
+
ui.info("Model: #{session[:model]}")
|
|
67
|
+
ui.info("Messages: #{session[:message_count]}")
|
|
68
|
+
ui.info("Tokens: #{session[:token_count]}")
|
|
69
|
+
ui.info("Created: #{session[:created_at]}")
|
|
70
|
+
ui.info("Updated: #{session[:updated_at]}")
|
|
71
|
+
|
|
72
|
+
return unless session[:parent_session_id]
|
|
73
|
+
|
|
74
|
+
ui.info("Parent: #{session[:parent_session_id]}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
desc "delete ID", "Permanently delete a session and all its messages/events"
|
|
78
|
+
option :force, type: :boolean, default: false, aliases: "-f",
|
|
79
|
+
desc: "Skip the confirmation prompt"
|
|
80
|
+
def delete(id)
|
|
81
|
+
Rubino.ensure_database_ready!
|
|
82
|
+
repo = Session::Repository.new
|
|
83
|
+
session = repo.find(id)
|
|
84
|
+
|
|
85
|
+
# Single-styled not-found error (#20), as in #show above.
|
|
86
|
+
raise Thor::Error, "session not found: #{id}" if session.nil?
|
|
87
|
+
|
|
88
|
+
self.class.destroy_with_confirm(session, repo: repo, ui: Rubino.ui, force: options[:force])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# ONE confirm-and-destroy flow for both surfaces (#183): the CLI verb
|
|
92
|
+
# above and the in-chat `/sessions delete <id>`.
|
|
93
|
+
def self.destroy_with_confirm(session, repo:, ui:, force: false)
|
|
94
|
+
unless force
|
|
95
|
+
confirmed = ui.confirm_destructive(
|
|
96
|
+
"Delete session #{session[:id][0..7]} '#{session[:title] || "(untitled)"}'? " \
|
|
97
|
+
"This will also remove its messages, events, and tool calls."
|
|
98
|
+
)
|
|
99
|
+
unless confirmed
|
|
100
|
+
ui.info("Aborted.")
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
repo.destroy!(session[:id])
|
|
106
|
+
ui.success("Deleted session #{session[:id][0..7]}.")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
desc "compact ID", "Manually trigger compaction on a session"
|
|
110
|
+
def compact(id)
|
|
111
|
+
Rubino.ensure_database_ready!
|
|
112
|
+
repo = Session::Repository.new
|
|
113
|
+
session = repo.find(id)
|
|
114
|
+
|
|
115
|
+
# Single-styled not-found error (#20), as in #show above.
|
|
116
|
+
raise Thor::Error, "session not found: #{id}" if session.nil?
|
|
117
|
+
|
|
118
|
+
Rubino.ui.info("Compacting session #{id}...")
|
|
119
|
+
compressor = Context::Compressor.new(session_id: id)
|
|
120
|
+
result = compressor.compact!
|
|
121
|
+
Rubino.ui.compression_finished(result)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|