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,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module Commands
|
|
7
|
+
module Handlers
|
|
8
|
+
# The `/sessions` list/show/delete/picker surface plus the `/probe` and
|
|
9
|
+
# `/branch` REPL signals, extracted from Commands::Executor (batch B).
|
|
10
|
+
#
|
|
11
|
+
# No-arg = list recent + how to resume; arg = resolve and resume in place.
|
|
12
|
+
# Resuming returns a {resume_session_id:} signal the REPL acts on by
|
|
13
|
+
# rebuilding its runner on that session (history replays). Reuses
|
|
14
|
+
# Session::Repository#list and #find_by_id_or_title (which already raises
|
|
15
|
+
# AmbiguousSessionError on >1 match).
|
|
16
|
+
#
|
|
17
|
+
# The management verbs (#183) reuse the CLI subcommands' logic
|
|
18
|
+
# (CLI::SessionCommand.render / .destroy_with_confirm — ONE rendering and
|
|
19
|
+
# ONE delete flow for both surfaces):
|
|
20
|
+
#
|
|
21
|
+
# /sessions → list (picker on a TTY) + resume
|
|
22
|
+
# /sessions --all → list without the row cap
|
|
23
|
+
# /sessions show <id> → details, without switching into it
|
|
24
|
+
# /sessions delete <id> → delete (asks to confirm)
|
|
25
|
+
# /sessions <id|title> → resume
|
|
26
|
+
class Sessions
|
|
27
|
+
def initialize(ui:, runner:)
|
|
28
|
+
@ui = ui
|
|
29
|
+
@runner = runner
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def handle_sessions(arguments)
|
|
33
|
+
tokens = arguments.to_s.strip.split(/\s+/)
|
|
34
|
+
all = tokens.delete("--all") ? true : false
|
|
35
|
+
return list_sessions(all: all) if tokens.empty?
|
|
36
|
+
|
|
37
|
+
case tokens.first
|
|
38
|
+
when "show" then session_verb(tokens[1..].join(" "), "show") { |s| CLI::SessionCommand.render(s, ui: @ui) }
|
|
39
|
+
when "delete" then session_verb(tokens[1..].join(" "), "delete") { |s| delete_session(s) }
|
|
40
|
+
else resume_session(tokens.join(" "))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# `/probe <text>` — the discoverable alias for the `? ` prefix. Bare
|
|
45
|
+
# `/probe` only teaches the prefix (the one-keystroke common case); with
|
|
46
|
+
# text, signal the REPL to run the ephemeral side-inference and discard.
|
|
47
|
+
def handle_probe(arguments)
|
|
48
|
+
text = arguments.to_s.strip
|
|
49
|
+
if text.empty?
|
|
50
|
+
@ui.info("Ask an ephemeral side-question that is NOT saved to this session.")
|
|
51
|
+
@ui.info("Tip: just start a line with '? ' — e.g. ? is this lib MIT or GPL?")
|
|
52
|
+
return :handled
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
{ probe: text }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# `/branch [name]` — fork the current session here into a NEW saved one
|
|
59
|
+
# and switch into it. The REPL holds the runner/session, so we just pass
|
|
60
|
+
# the optional title along on the branch signal.
|
|
61
|
+
def handle_branch(arguments)
|
|
62
|
+
title = arguments.to_s.strip
|
|
63
|
+
{ branch: true, title: title.empty? ? nil : title }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Resolves the id/title for a /sessions verb (same matcher resume uses,
|
|
69
|
+
# so short ids and title substrings work) and yields the session row;
|
|
70
|
+
# prints the usage/not-found/ambiguous error otherwise. Always :handled —
|
|
71
|
+
# the verbs never fall through to the unknown-command path (#34).
|
|
72
|
+
def session_verb(query, verb)
|
|
73
|
+
if query.nil? || query.empty?
|
|
74
|
+
@ui.info("Usage: /sessions #{verb} <id>")
|
|
75
|
+
return :handled
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
session = Session::Repository.new.find_by_id_or_title(query)
|
|
79
|
+
if session.nil?
|
|
80
|
+
@ui.error("no session matching #{query.inspect}.")
|
|
81
|
+
@ui.info("List them with /sessions")
|
|
82
|
+
else
|
|
83
|
+
yield session
|
|
84
|
+
end
|
|
85
|
+
:handled
|
|
86
|
+
rescue Rubino::AmbiguousSessionError => e
|
|
87
|
+
@ui.error(e.message)
|
|
88
|
+
:handled
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Deletes a session in-chat via the SAME confirm-and-destroy flow the
|
|
92
|
+
# `rubino sessions delete` CLI verb runs (#183). The session the live
|
|
93
|
+
# runner sits on is refused — deleting the history under the active
|
|
94
|
+
# runner would corrupt the running conversation; /new first.
|
|
95
|
+
def delete_session(session)
|
|
96
|
+
if @runner&.session&.dig(:id) == session[:id]
|
|
97
|
+
@ui.error("that is the ACTIVE session — start a new one first (/new), then delete it.")
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
CLI::SessionCommand.destroy_with_confirm(session, repo: Session::Repository.new, ui: @ui)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def list_sessions(all: false)
|
|
105
|
+
sessions = Session::Repository.new.list(limit: all ? nil : sessions_list_limit)
|
|
106
|
+
if sessions.empty?
|
|
107
|
+
@ui.info("No past sessions yet.")
|
|
108
|
+
return :handled
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# ONE surface, not two (#40): on a real terminal the arrow-key picker
|
|
112
|
+
# IS the list (Enter resumes, Esc cancels — #73, letters filter), with
|
|
113
|
+
# Created/Status folded into each row, so the same sessions are never
|
|
114
|
+
# rendered twice (static table + picker). Off a TTY the static table +
|
|
115
|
+
# typed-shortcut fallback renders instead.
|
|
116
|
+
return sessions_table_fallback(sessions) unless interactive_terminal?
|
|
117
|
+
|
|
118
|
+
choices = sessions.map { |s| [session_choice_label(s), s[:id]] }
|
|
119
|
+
chosen = @ui.select("Resume which session? (Esc to cancel)", choices)
|
|
120
|
+
if chosen
|
|
121
|
+
session = sessions.find { |s| s[:id] == chosen }
|
|
122
|
+
@ui.success(%(Resuming #{chosen[0..7]} "#{session_title(session)}")) if session
|
|
123
|
+
return { resume_session_id: chosen }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@ui.info("Resume: /sessions <id|title> · /sessions show|delete <id>")
|
|
127
|
+
:handled
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Static fallback for non-interactive callers (pipes / Null UI): the
|
|
131
|
+
# bordered table the picker replaces on a TTY. Leads with the identifying
|
|
132
|
+
# fields (ID, Title, Created) so a narrow-term card fallback scans well —
|
|
133
|
+
# the key field first, not buried (#84).
|
|
134
|
+
def sessions_table_fallback(sessions)
|
|
135
|
+
rows = sessions.map do |s|
|
|
136
|
+
[s[:id].to_s[0..7], session_title(s), s[:created_at].to_s, s[:status].to_s, s[:message_count].to_s]
|
|
137
|
+
end
|
|
138
|
+
@ui.table(headers: %w[ID Title Created Status Msgs], rows: rows)
|
|
139
|
+
@ui.info("Resume: /sessions <id|title> · /sessions show|delete <id>")
|
|
140
|
+
:handled
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# One picker row: short id + title + message count + recency (and status
|
|
144
|
+
# when not yet ended), so the highlighted entry is identifiable at a
|
|
145
|
+
# glance and the picker is a clean superset of the old static table (#40).
|
|
146
|
+
def session_choice_label(session)
|
|
147
|
+
id = session[:id].to_s[0..7]
|
|
148
|
+
title = session_title(session)
|
|
149
|
+
msgs = session[:message_count]
|
|
150
|
+
meta = [
|
|
151
|
+
("#{msgs} msg#{"s" if msgs != 1}" if msgs),
|
|
152
|
+
session_age(session),
|
|
153
|
+
(session[:status].to_s unless ["", "ended"].include?(session[:status].to_s))
|
|
154
|
+
].compact.join(" · ")
|
|
155
|
+
meta.empty? ? "#{id} #{title}" : "#{id} #{title} (#{meta})"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# "Created" humanized for the picker row — "5m ago" scans better than a
|
|
159
|
+
# raw ISO timestamp in a recency-ordered list (#40). nil when unparseable.
|
|
160
|
+
def session_age(session)
|
|
161
|
+
created = session[:created_at]
|
|
162
|
+
created = Time.parse(created.to_s) unless created.is_a?(Time)
|
|
163
|
+
"#{Rubino::Util::Duration.human_duration(Time.now - created)} ago"
|
|
164
|
+
rescue StandardError
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def resume_session(query)
|
|
169
|
+
session = Session::Repository.new.find_by_id_or_title(query)
|
|
170
|
+
if session.nil?
|
|
171
|
+
@ui.error("no session matching #{query.inspect}.")
|
|
172
|
+
@ui.info("List them with /sessions")
|
|
173
|
+
return :handled
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
@ui.success(%(Resuming #{session[:id][0..7]} "#{session_title(session)}"))
|
|
177
|
+
{ resume_session_id: session[:id] }
|
|
178
|
+
rescue Rubino::AmbiguousSessionError => e
|
|
179
|
+
@ui.error(e.message)
|
|
180
|
+
:handled
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def session_title(session)
|
|
184
|
+
title = session[:title].to_s.strip
|
|
185
|
+
title.empty? ? "(untitled)" : title
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The bare-list row cap (#183): configurable (`sessions.list_limit`) and
|
|
189
|
+
# liftable per call with `/sessions --all` — no longer hardwired to 10.
|
|
190
|
+
def sessions_list_limit
|
|
191
|
+
limit = Rubino.configuration.dig("sessions", "list_limit").to_i
|
|
192
|
+
limit.positive? ? limit : 10
|
|
193
|
+
rescue StandardError
|
|
194
|
+
10
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# True when the REPL owns a real interactive terminal (so the arrow-key
|
|
198
|
+
# picker makes sense). Off a TTY we render the static table fallback.
|
|
199
|
+
def interactive_terminal?
|
|
200
|
+
$stdin.respond_to?(:tty?) && $stdin.tty? && $stdout.respond_to?(:tty?) && $stdout.tty?
|
|
201
|
+
rescue StandardError
|
|
202
|
+
false
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Commands
|
|
5
|
+
module Handlers
|
|
6
|
+
# The `/skills` list/activate/enable/disable surface, extracted from
|
|
7
|
+
# Commands::Executor (batch B).
|
|
8
|
+
#
|
|
9
|
+
# `/skills` → list (unchanged behavior).
|
|
10
|
+
# `/skills <name>` → ACTIVATE that skill for the session (sticky).
|
|
11
|
+
# The name is validated against the registry; an
|
|
12
|
+
# unknown OR DISABLED name errors and leaves the
|
|
13
|
+
# active skill unchanged.
|
|
14
|
+
# `/skills none` → CLEAR the active skill (also the `✗ none`
|
|
15
|
+
# picker entry, whose spliced label is
|
|
16
|
+
# normalized here).
|
|
17
|
+
# `/skills enable <name>` → persistently re-enable a skill (#188) — the
|
|
18
|
+
# `/skills disable <name>` same StateRepository write the HTTP API
|
|
19
|
+
# toggle and the `rubino skills` CLI verbs run
|
|
20
|
+
# (Skills::Toggle), affecting EVERY session,
|
|
21
|
+
# unlike the session-scoped activation.
|
|
22
|
+
#
|
|
23
|
+
# The active skill is stored in Rubino::ActiveSkill (a process-level slot,
|
|
24
|
+
# mirroring Rubino::Modes) so it survives across turns and is force-loaded
|
|
25
|
+
# into the system prompt each turn (Context::PromptAssembler).
|
|
26
|
+
class Skills
|
|
27
|
+
# The /skills toggle verbs (#188) — the same registry-validated
|
|
28
|
+
# StateRepository write the HTTP API and `rubino skills` CLI run.
|
|
29
|
+
TOGGLE_VERBS = %w[enable disable].freeze
|
|
30
|
+
|
|
31
|
+
def initialize(ui:)
|
|
32
|
+
@ui = ui
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def handle_skills(arguments)
|
|
36
|
+
tokens = arguments.to_s.strip.split(/\s+/)
|
|
37
|
+
if TOGGLE_VERBS.include?(tokens.first.to_s.downcase)
|
|
38
|
+
toggle_skill(tokens[1], enabled: tokens.first.casecmp?("enable"))
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
arg = normalize_skill_arg(arguments)
|
|
43
|
+
|
|
44
|
+
return show_skills if arg.nil?
|
|
45
|
+
|
|
46
|
+
if clear_skill_arg?(arg)
|
|
47
|
+
previous = Rubino::ActiveSkill.current
|
|
48
|
+
Rubino::ActiveSkill.clear
|
|
49
|
+
if previous
|
|
50
|
+
@ui.success("Cleared active skill (was: #{previous}).")
|
|
51
|
+
else
|
|
52
|
+
@ui.info("No active skill.")
|
|
53
|
+
end
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Trust-aligned discovery (#63): activate only skills the assembler
|
|
58
|
+
# will actually pin — in an untrusted cwd a project-local skill is
|
|
59
|
+
# refused (with a reason) instead of chip-active-but-not-injected.
|
|
60
|
+
registry = Rubino::Skills::Registry.trusted
|
|
61
|
+
skill = registry.find(arg)
|
|
62
|
+
unless skill
|
|
63
|
+
if Rubino::Skills::Registry.new.find(arg)
|
|
64
|
+
@ui.error("skill #{arg} is in this directory's .rubino/skills, but the directory " \
|
|
65
|
+
"isn't trusted — its SKILL.md would not be loaded, so it can't be activated")
|
|
66
|
+
else
|
|
67
|
+
@ui.error("unknown skill: #{arg}")
|
|
68
|
+
available = registry.names
|
|
69
|
+
@ui.info("Available: #{available.join(", ")}") unless available.empty?
|
|
70
|
+
end
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# A disabled skill is EXCLUDED from activation (#188): the assembler
|
|
75
|
+
# refuses to inject it (active_skill_block checks enabled?), so pinning
|
|
76
|
+
# it would show an active chip with no effect.
|
|
77
|
+
unless registry.enabled?(skill.name)
|
|
78
|
+
@ui.error("skill #{skill.name} is disabled — /skills enable #{skill.name} to use it")
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Rubino::ActiveSkill.set(skill.name)
|
|
83
|
+
@ui.success("Active skill: #{skill.name} (loaded into context for this session).")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# `/skills enable|disable <name>` (#188) — the missing human surface for
|
|
89
|
+
# the StateRepository toggle (previously HTTP-API-only). Persisted, so it
|
|
90
|
+
# affects the Level-1 index of every session until toggled back.
|
|
91
|
+
def toggle_skill(name, enabled:)
|
|
92
|
+
verb = enabled ? "enable" : "disable"
|
|
93
|
+
if name.to_s.strip.empty?
|
|
94
|
+
@ui.info("Usage: /skills #{verb} <name>")
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
registry = Rubino::Skills::Registry.trusted
|
|
99
|
+
unless Rubino::Skills::Toggle.set(name, enabled: enabled, registry: registry)
|
|
100
|
+
@ui.error("unknown skill: #{name}")
|
|
101
|
+
available = registry.names
|
|
102
|
+
@ui.info("Available: #{available.join(", ")}") unless available.empty?
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if enabled
|
|
107
|
+
@ui.success("Enabled skill: #{name} (back in the skills index for every session).")
|
|
108
|
+
else
|
|
109
|
+
clear_disabled_active_skill(name)
|
|
110
|
+
@ui.success("Disabled skill: #{name} (out of the index for every session; " \
|
|
111
|
+
"/skills enable #{name} to restore).")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Disabling the skill that is currently PINNED active would leave a lying
|
|
116
|
+
# chip — the assembler silently drops a disabled active skill — so the
|
|
117
|
+
# pin is cleared with a note instead.
|
|
118
|
+
def clear_disabled_active_skill(name)
|
|
119
|
+
return unless Rubino::ActiveSkill.current == name
|
|
120
|
+
|
|
121
|
+
Rubino::ActiveSkill.clear
|
|
122
|
+
@ui.info("(it was the active skill — pin cleared)")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The single argument to `/skills`, trimmed; nil when no argument was
|
|
126
|
+
# given (bare `/skills` → list). The picker splices the `✗ none` label, so
|
|
127
|
+
# the leading `✗ ` marker is stripped here to recover the bare token.
|
|
128
|
+
def normalize_skill_arg(arguments)
|
|
129
|
+
raw = arguments.to_s.strip.sub(/\A✗\s+/, "")
|
|
130
|
+
# Only the FIRST token is the skill name (skill names are single tokens).
|
|
131
|
+
token = raw.split(/\s+/).first
|
|
132
|
+
token unless token.nil? || token.empty?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# True when the argument means "clear the active skill" (the `none`
|
|
136
|
+
# sentinel, case-insensitive — the `✗ ` marker was already stripped).
|
|
137
|
+
def clear_skill_arg?(arg)
|
|
138
|
+
arg.casecmp?(Rubino::ActiveSkill::NONE)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def show_skills
|
|
142
|
+
registry = Rubino::Skills::Registry.trusted
|
|
143
|
+
skills = registry.all
|
|
144
|
+
if skills.empty?
|
|
145
|
+
@ui.info("No skills found.")
|
|
146
|
+
@ui.info("Add .md files to .rubino/skills/ to create skills.")
|
|
147
|
+
else
|
|
148
|
+
active = Rubino::ActiveSkill.current
|
|
149
|
+
skills.each do |skill|
|
|
150
|
+
status = registry.enabled?(skill.name) ? "" : " (disabled)"
|
|
151
|
+
status += " (active)" if active && active == skill.name
|
|
152
|
+
head = " #{skill.name}#{status} - "
|
|
153
|
+
# Word-wrap the description so a long one breaks on spaces instead of
|
|
154
|
+
# being hard-wrapped mid-word by the terminal at the right edge
|
|
155
|
+
# (B8 — "officia\nl"). Continuation lines hang-indent under the
|
|
156
|
+
# description so the list stays readable.
|
|
157
|
+
wrap_skill_line(head, skill.description.to_s).each { |line| @ui.info(line) }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Wraps "<head><description>" to the terminal width, breaking only on
|
|
163
|
+
# whitespace, with continuation lines indented to the description column.
|
|
164
|
+
def wrap_skill_line(head, description)
|
|
165
|
+
width = terminal_width
|
|
166
|
+
indent = " " * head.length
|
|
167
|
+
avail = [width - head.length, 20].max
|
|
168
|
+
|
|
169
|
+
lines = []
|
|
170
|
+
current = +""
|
|
171
|
+
description.split(/\s+/).each do |word|
|
|
172
|
+
candidate = current.empty? ? word : "#{current} #{word}"
|
|
173
|
+
if candidate.length > avail && !current.empty?
|
|
174
|
+
lines << current
|
|
175
|
+
current = word.dup
|
|
176
|
+
else
|
|
177
|
+
current = candidate
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
lines << current unless current.empty?
|
|
181
|
+
lines = [""] if lines.empty?
|
|
182
|
+
|
|
183
|
+
lines.each_with_index.map { |line, i| (i.zero? ? head : indent) + line }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def terminal_width
|
|
187
|
+
cols = IO.console&.winsize&.last
|
|
188
|
+
cols&.positive? ? cols : 80
|
|
189
|
+
rescue StandardError
|
|
190
|
+
80
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Commands
|
|
5
|
+
module Handlers
|
|
6
|
+
# The `/status` at-a-glance state panel, extracted from Commands::Executor
|
|
7
|
+
# (batch B). Assembles the model/mode/session lines plus approval policy,
|
|
8
|
+
# provider/connection, and the tool/mcp/memory/skills rosters over the same
|
|
9
|
+
# services (Modes, Session::Repository, Memory backend, BackgroundTasks,
|
|
10
|
+
# Skills::Registry). A plain collaborator given the live `ui`/`runner`.
|
|
11
|
+
class Status
|
|
12
|
+
def initialize(ui:, runner:)
|
|
13
|
+
@ui = ui
|
|
14
|
+
@runner = runner
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Labels dim, values plain, cyan only on the actionable pointers (P8).
|
|
18
|
+
def show_status
|
|
19
|
+
@ui.separator
|
|
20
|
+
@ui.panel_line("model", status_model)
|
|
21
|
+
@ui.panel_line("provider", status_provider_line)
|
|
22
|
+
@ui.panel_line("mode", "#{Rubino::Modes.current} — #{Rubino::Modes.description}")
|
|
23
|
+
@ui.panel_line("display", status_display_line, pointer: "(use /reasoning · /think)")
|
|
24
|
+
@ui.panel_line("approvals", status_approvals_line)
|
|
25
|
+
@ui.panel_line("session", status_session_line)
|
|
26
|
+
@ui.panel_line("tools", status_tools_line)
|
|
27
|
+
# MCP only when servers are configured (#182/#186) — a non-MCP user's
|
|
28
|
+
# /status stays exactly as before, and MCP tools stop being invisibly
|
|
29
|
+
# mixed into the truncated tools line as the only trace of MCP.
|
|
30
|
+
@ui.panel_line("mcp", status_mcp_line, pointer: "(use /mcp)") if Rubino::MCP.enabled?
|
|
31
|
+
if (dirs = status_dirs_line)
|
|
32
|
+
@ui.panel_line("dirs", dirs, pointer: "(use /dirs)")
|
|
33
|
+
end
|
|
34
|
+
@ui.panel_line("memory", status_memory_line, pointer: "(use /memory)")
|
|
35
|
+
@ui.panel_line("skills", status_skills_line, pointer: "(use /skills)")
|
|
36
|
+
@ui.panel_line("background", status_background_line, pointer: "(use /agents)")
|
|
37
|
+
if (jobs = status_jobs_line)
|
|
38
|
+
@ui.panel_line("jobs", jobs, pointer: "(use /jobs)")
|
|
39
|
+
end
|
|
40
|
+
@ui.separator
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# The persisted display prefs (#186): /reasoning and /think write config
|
|
46
|
+
# but were invisible — not in the chip, not in /status.
|
|
47
|
+
def status_display_line
|
|
48
|
+
mode = Rubino::Config::ReasoningPrefs.mode(Rubino.configuration)
|
|
49
|
+
effort = Rubino::Config::ReasoningPrefs.effort(Rubino.configuration) ||
|
|
50
|
+
Rubino::Config::ReasoningPrefs::DEFAULT_EFFORT
|
|
51
|
+
"reasoning: #{mode} · effort: #{effort}"
|
|
52
|
+
rescue StandardError
|
|
53
|
+
"(unavailable)"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Workspace roots + trust (#186) — trust is the #1 "why are my
|
|
57
|
+
# skills/AGENTS.md not loading" confusion. Only earns a line when there
|
|
58
|
+
# is something to say (>1 root or any untrusted); nil otherwise.
|
|
59
|
+
def status_dirs_line
|
|
60
|
+
roots = Rubino::Workspace.canonical_roots
|
|
61
|
+
untrusted = roots.count { |d| !Rubino::Trust.trusted?(d) }
|
|
62
|
+
return nil if roots.size <= 1 && untrusted.zero?
|
|
63
|
+
|
|
64
|
+
line = "#{roots.size} root#{"s" if roots.size != 1}"
|
|
65
|
+
untrusted.positive? ? "#{line} · #{untrusted} untrusted (context/skills withheld)" : line
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The persistent jobs queue (#186) — distinct from the in-process
|
|
71
|
+
# `background` subagents line. Only earns a line when nonzero; nil (no
|
|
72
|
+
# line) when the queue is empty or unreadable.
|
|
73
|
+
def status_jobs_line
|
|
74
|
+
queue = Rubino::Jobs::Queue.new
|
|
75
|
+
pending = queue.pending_count
|
|
76
|
+
failed = queue.failed_count
|
|
77
|
+
return nil unless pending.positive? || failed.positive?
|
|
78
|
+
|
|
79
|
+
[("#{pending} pending" if pending.positive?),
|
|
80
|
+
("#{failed} failed" if failed.positive?)].compact.join(" · ")
|
|
81
|
+
rescue StandardError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def status_model
|
|
86
|
+
@runner&.session&.dig(:model) ||
|
|
87
|
+
(@runner.respond_to?(:model_id) ? @runner.model_id : nil) ||
|
|
88
|
+
Rubino.configuration.model_default
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The configured provider — the "what am I talking to" line a status
|
|
92
|
+
# check wants. We report the configured target, not a live probe (a
|
|
93
|
+
# health round-trip would make /status slow and flaky).
|
|
94
|
+
def status_provider_line
|
|
95
|
+
Rubino.configuration.model_provider || "(default)"
|
|
96
|
+
rescue StandardError
|
|
97
|
+
"(unavailable)"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# One-line approval-policy summary so a newcomer knows what will prompt.
|
|
101
|
+
# Mode is authoritative: yolo skips every approval, plan filters mutating
|
|
102
|
+
# tools out entirely; otherwise approvals come from config.
|
|
103
|
+
def status_approvals_line
|
|
104
|
+
case Rubino::Modes.current
|
|
105
|
+
when :yolo then "skipped (yolo mode — nothing prompts)"
|
|
106
|
+
when :plan then "read-only mode — no edits/shell to approve"
|
|
107
|
+
else "from config (mutating commands prompt)"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A compact roster of the tools the agent can actually use right now
|
|
112
|
+
# (mode filters the registry), so /status answers "what can it DO".
|
|
113
|
+
def status_tools_line
|
|
114
|
+
names = Tools::Registry.instance.enabled_tools.map(&:name).sort
|
|
115
|
+
return "(none)" if names.empty?
|
|
116
|
+
|
|
117
|
+
truncate(names.join(", "), 64)
|
|
118
|
+
rescue StandardError
|
|
119
|
+
"(unavailable)"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# `2 servers · 1 reachable · 14 tools` — reads the LIVE booted manager
|
|
123
|
+
# (no client → 0 reachable), never re-spawns servers.
|
|
124
|
+
def status_mcp_line
|
|
125
|
+
servers = mcp_servers_config.size
|
|
126
|
+
reachable = mcp_health.count { |h| h[:alive] }
|
|
127
|
+
tools = Tools::Registry.all.count { |t| t.is_a?(Rubino::MCP::MCPToolWrapper) }
|
|
128
|
+
"#{servers} server#{"s" if servers != 1} · #{reachable} reachable · #{tools} tool#{"s" if tools != 1}"
|
|
129
|
+
rescue StandardError
|
|
130
|
+
"(unavailable)"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def status_session_line
|
|
134
|
+
session = @runner&.session
|
|
135
|
+
return "(none)" unless session
|
|
136
|
+
|
|
137
|
+
id = session[:id].to_s[0..7]
|
|
138
|
+
title = session[:title].to_s.strip
|
|
139
|
+
title = title.empty? ? "(untitled)" : %("#{title}")
|
|
140
|
+
msgs = status_message_count(session)
|
|
141
|
+
"#{id} #{title}#{" · #{msgs} msgs" if msgs}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The session's message count, read LIVE from the message store. The
|
|
145
|
+
# in-memory session hash's :message_count is a boot-time snapshot the
|
|
146
|
+
# streaming path never refreshes, so /status reported a permanent
|
|
147
|
+
# "0 msgs" while the DB had every turn (#159). Counting the persisted
|
|
148
|
+
# rows also matches the "Loaded N prior messages" resume banner.
|
|
149
|
+
def status_message_count(session)
|
|
150
|
+
Session::Store.new.count(session[:id])
|
|
151
|
+
rescue StandardError
|
|
152
|
+
session[:message_count]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# /status must count facts on the ACTIVE backend — the same store /memory
|
|
156
|
+
# and the `rubino memory` CLI read via Memory::Backends.build — not the
|
|
157
|
+
# legacy `:memories` table Memory::Store is hardwired to (#83).
|
|
158
|
+
def status_memory_line
|
|
159
|
+
backend = Rubino.configuration.dig("memory", "backend") || Rubino::Memory::Backends::DEFAULT_NAME
|
|
160
|
+
"backend: #{backend} · #{memory_backend.count} facts"
|
|
161
|
+
rescue StandardError
|
|
162
|
+
"(unavailable)"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def status_skills_line
|
|
166
|
+
registry = Rubino::Skills::Registry.trusted
|
|
167
|
+
all = registry.all
|
|
168
|
+
enabled = all.count { |s| registry.enabled?(s.name) }
|
|
169
|
+
line = "#{all.size} available, #{enabled} enabled"
|
|
170
|
+
# WHICH skill is pinned (#186) — the chip shows it but the canonical
|
|
171
|
+
# state dump omitted it.
|
|
172
|
+
active = Rubino::ActiveSkill.current
|
|
173
|
+
active ? "#{line} · active: #{active}" : line
|
|
174
|
+
rescue StandardError
|
|
175
|
+
"(unavailable)"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def status_background_line
|
|
179
|
+
entries = Tools::BackgroundTasks.instance.list
|
|
180
|
+
running = entries.count { |e| e.status == :running }
|
|
181
|
+
ids = entries.first(3).map(&:id).join(", ")
|
|
182
|
+
line = "#{running} running · #{entries.size} total"
|
|
183
|
+
ids.empty? ? line : "#{line} (#{ids})"
|
|
184
|
+
rescue StandardError
|
|
185
|
+
"(unavailable)"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Resolve the *configured* memory backend (default: sqlite tiny-Zep) for
|
|
189
|
+
# the fact count — the same store the agent loop and /memory read.
|
|
190
|
+
def memory_backend
|
|
191
|
+
@memory_backend ||= Rubino::Memory::Backends.build
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The configured mcp.servers block (name => config), {} when absent.
|
|
195
|
+
def mcp_servers_config
|
|
196
|
+
Rubino.configuration.dig("mcp", "servers") || {}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Live reachability from the booted manager; [] when MCP never booted.
|
|
200
|
+
def mcp_health
|
|
201
|
+
Rubino::MCP.manager&.health_check || []
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def truncate(text, max)
|
|
205
|
+
s = text.to_s.gsub(/\s+/, " ").strip
|
|
206
|
+
s.length > max ? "#{s[0, max - 1]}…" : s
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|