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,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module CLI
|
|
7
|
+
# Handles initial setup: creates config directory, default config,
|
|
8
|
+
# initializes the database, and runs migrations.
|
|
9
|
+
class SetupCommand
|
|
10
|
+
def execute
|
|
11
|
+
ui = Rubino.ui
|
|
12
|
+
|
|
13
|
+
ui.info("Setting up rubino...")
|
|
14
|
+
ui.blank_line
|
|
15
|
+
|
|
16
|
+
# Create home directory (0700 — only the owner sees stored secrets)
|
|
17
|
+
# and subdirectories. ensure_directories! owns the mkdir + chmod so
|
|
18
|
+
# every entry point that materializes the home agrees on 0700 (#65).
|
|
19
|
+
home = Rubino.home_path
|
|
20
|
+
Rubino.ensure_directories!
|
|
21
|
+
ui.success("Home directory: #{home}")
|
|
22
|
+
ui.success("Subdirectories created")
|
|
23
|
+
|
|
24
|
+
# Create config file if it doesn't exist
|
|
25
|
+
loader = Config::Loader.new
|
|
26
|
+
if loader.config_exists?
|
|
27
|
+
ui.warning("Config already exists: #{loader.config_path}")
|
|
28
|
+
else
|
|
29
|
+
loader.create_default_config!
|
|
30
|
+
File.chmod(0o600, loader.config_path)
|
|
31
|
+
ui.success("Config created: #{loader.config_path}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Create .env template if it doesn't exist (0600 — contains api keys)
|
|
35
|
+
env_path = File.join(home, ".env")
|
|
36
|
+
unless File.exist?(env_path)
|
|
37
|
+
File.write(env_path, env_template)
|
|
38
|
+
File.chmod(0o600, env_path)
|
|
39
|
+
ui.success("Env template created: #{env_path}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Initialize database
|
|
43
|
+
ui.status("Initializing database...")
|
|
44
|
+
connection = Rubino.database
|
|
45
|
+
migrator = Database::Migrator.new(connection)
|
|
46
|
+
migrator.migrate!
|
|
47
|
+
ui.success("Database initialized: #{connection.db_path}")
|
|
48
|
+
|
|
49
|
+
# First-run onboarding: if no usable key is configured yet AND we're on
|
|
50
|
+
# a real TTY, guide the user to a working model (provider/model/key)
|
|
51
|
+
# right here so `setup` ends in a usable config — not a dead-end that
|
|
52
|
+
# still needs hand-editing config.yml (#93). Non-interactive setup keeps
|
|
53
|
+
# the old behaviour (files created, no prompts).
|
|
54
|
+
maybe_run_onboarding(ui)
|
|
55
|
+
|
|
56
|
+
ui.blank_line
|
|
57
|
+
# Tell the truth about the end state (#31). A green "Setup complete!" is
|
|
58
|
+
# only honest when a usable credential is actually configured — printing
|
|
59
|
+
# it after a skipped/abandoned onboarding (no provider, no key) directly
|
|
60
|
+
# contradicts the state. Re-check the credential after onboarding so the
|
|
61
|
+
# final line reflects reality on both the interactive and the
|
|
62
|
+
# non-interactive (files-only) paths.
|
|
63
|
+
if LLM::CredentialCheck.usable?
|
|
64
|
+
ui.success("Setup complete! Run 'rubino doctor' to verify.")
|
|
65
|
+
else
|
|
66
|
+
ui.warning("Setup files created, but no model is configured yet.")
|
|
67
|
+
ui.status("Run 'rubino setup' again or add an API key, then 'rubino doctor' to verify.")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def maybe_run_onboarding(ui)
|
|
74
|
+
return unless interactive?
|
|
75
|
+
return if LLM::CredentialCheck.usable?
|
|
76
|
+
|
|
77
|
+
OnboardingWizard.new(ui: ui).run
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def interactive?
|
|
81
|
+
$stdin.tty? && $stdout.tty?
|
|
82
|
+
rescue StandardError
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def env_template
|
|
87
|
+
<<~ENV
|
|
88
|
+
# Rubino API Keys
|
|
89
|
+
# Add your API keys here. Do NOT commit this file.
|
|
90
|
+
# `rubino setup` (on a terminal) can fill one in for you.
|
|
91
|
+
|
|
92
|
+
# MiniMax (recommended default — Anthropic-compatible)
|
|
93
|
+
# MINIMAX_API_KEY=...
|
|
94
|
+
|
|
95
|
+
# OpenAI
|
|
96
|
+
# OPENAI_API_KEY=sk-...
|
|
97
|
+
|
|
98
|
+
# Anthropic
|
|
99
|
+
# ANTHROPIC_API_KEY=sk-ant-...
|
|
100
|
+
|
|
101
|
+
# Google
|
|
102
|
+
# GEMINI_API_KEY=...
|
|
103
|
+
ENV
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module CLI
|
|
7
|
+
# Subcommands for managing skills (#188). `list` mirrors the in-chat
|
|
8
|
+
# /skills disclosure (enabled/disabled markers), `show` prints a skill's
|
|
9
|
+
# SKILL.md body (trust review before enabling), and `enable`/`disable`
|
|
10
|
+
# run the SAME registry-validated StateRepository write the HTTP API
|
|
11
|
+
# toggle and the in-chat `/skills enable|disable` use (Skills::Toggle) —
|
|
12
|
+
# no new logic, just the missing terminal surface.
|
|
13
|
+
class SkillsCommand < Thor
|
|
14
|
+
# Clean `tree`/help label instead of the underscored class-name default (F12).
|
|
15
|
+
namespace "rubino skills"
|
|
16
|
+
|
|
17
|
+
def self.exit_on_failure?
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "list", "List skills with enabled/disabled markers"
|
|
22
|
+
def list
|
|
23
|
+
Rubino.ensure_database_ready!
|
|
24
|
+
registry = Skills::Registry.trusted
|
|
25
|
+
skills = registry.all
|
|
26
|
+
if skills.empty?
|
|
27
|
+
Rubino.ui.info("No skills found.")
|
|
28
|
+
Rubino.ui.info("Add .md files to .rubino/skills/ to create skills.")
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
rows = skills.map do |skill|
|
|
33
|
+
[skill.name, skill_status(skill.name, registry), skill.description.to_s]
|
|
34
|
+
end
|
|
35
|
+
Rubino.ui.table(headers: %w[Name Status Description], rows: rows)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
desc "show NAME", "Print a skill's SKILL.md body (review it before enabling)"
|
|
39
|
+
def show(name)
|
|
40
|
+
skill = Skills::Registry.trusted.find(name)
|
|
41
|
+
if skill.nil?
|
|
42
|
+
Rubino.ui.error("unknown skill: #{name}")
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Rubino.ui.info(skill.content)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "enable NAME", "Enable a skill (back into the index, every session)"
|
|
50
|
+
def enable(name)
|
|
51
|
+
toggle(name, enabled: true)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
desc "disable NAME", "Disable a skill (drops out of the index, every session)"
|
|
55
|
+
def disable(name)
|
|
56
|
+
toggle(name, enabled: false)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# The Status cell: enabled/disabled from the StateRepository (the same
|
|
62
|
+
# source the in-chat list's "(disabled)" marker reads), plus the active
|
|
63
|
+
# pin when this process carries one (the slot is process-level, so a
|
|
64
|
+
# fresh CLI run normally shows none — the marker matters in-process).
|
|
65
|
+
def skill_status(name, registry)
|
|
66
|
+
status = registry.enabled?(name) ? "enabled" : "disabled"
|
|
67
|
+
status += " · active" if Rubino::ActiveSkill.current == name
|
|
68
|
+
status
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def toggle(name, enabled:)
|
|
72
|
+
Rubino.ensure_database_ready!
|
|
73
|
+
registry = Skills::Registry.trusted
|
|
74
|
+
unless Skills::Toggle.set(name, enabled: enabled, registry: registry)
|
|
75
|
+
Rubino.ui.error("unknown skill: #{name}")
|
|
76
|
+
available = registry.names
|
|
77
|
+
Rubino.ui.info("Available: #{available.join(", ")}") unless available.empty?
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
Rubino.ui.success("#{enabled ? "Enabled" : "Disabled"} skill: #{name}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module CLI
|
|
5
|
+
# Lists available tools and their status
|
|
6
|
+
class ToolsCommand
|
|
7
|
+
def execute
|
|
8
|
+
ui = Rubino.ui
|
|
9
|
+
config = Rubino.configuration
|
|
10
|
+
|
|
11
|
+
ui.info("Available Tools:")
|
|
12
|
+
ui.blank_line
|
|
13
|
+
|
|
14
|
+
# The registry is populated lazily when an agent runner boots; the bare
|
|
15
|
+
# `rubino tools` command never boots one, so without this the table
|
|
16
|
+
# is empty (F6). Registering the defaults is idempotent and matches what
|
|
17
|
+
# ChatCommand#ensure_setup! does before a turn.
|
|
18
|
+
Tools::Registry.register_defaults! if Tools::Registry.all.empty?
|
|
19
|
+
|
|
20
|
+
# Report against the SAME config gate the registry enforces: each row
|
|
21
|
+
# is a `tools.<config_key>` group, resolved exactly like
|
|
22
|
+
# Registry#tool_enabled_in_config? (opt-out — absent key = enabled).
|
|
23
|
+
# Deriving the rows from the registered tools' #config_key (rather
|
|
24
|
+
# than a hardcoded list) means the displayed state can never drift
|
|
25
|
+
# from reality — `web` no longer shows "disabled" while webfetch/
|
|
26
|
+
# websearch stay live, and the dead `browser` key is gone.
|
|
27
|
+
# MCP wrappers are excluded here: they are dynamic (no `tools.<key>`
|
|
28
|
+
# config gate) and get their own section below instead of fake rows
|
|
29
|
+
# in the config-group table.
|
|
30
|
+
builtins = Tools::Registry.all.grep_v(MCP::MCPToolWrapper)
|
|
31
|
+
config_keys = builtins.map(&:config_key).uniq
|
|
32
|
+
rows = config_keys.sort.map do |key|
|
|
33
|
+
value = config.dig("tools", key)
|
|
34
|
+
enabled = value.nil? || value == true
|
|
35
|
+
[key, enabled ? "enabled" : "disabled"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
ui.table(headers: %w[Tool Status], rows: rows)
|
|
39
|
+
|
|
40
|
+
print_enable_hint(rows)
|
|
41
|
+
print_mcp_tools
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# A disabled row with no pointer is a dead end (#20): name the exact
|
|
47
|
+
# config command that turns the group back on.
|
|
48
|
+
def print_enable_hint(rows)
|
|
49
|
+
disabled = rows.select { |_, status| status == "disabled" }.map(&:first)
|
|
50
|
+
return if disabled.empty?
|
|
51
|
+
|
|
52
|
+
ui = Rubino.ui
|
|
53
|
+
ui.blank_line
|
|
54
|
+
ui.info("Enable with: rubino config set tools.<name> true (e.g. tools.#{disabled.first})")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Lists tools from configured MCP servers (#91). Configuring
|
|
58
|
+
# `mcp.servers` is the opt-in: the Manager connects, prefixes each tool
|
|
59
|
+
# `servername_toolname`, and registers it alongside the built-ins. A
|
|
60
|
+
# configured-but-empty result still prints a breadcrumb (#94) so MCP
|
|
61
|
+
# users are never left staring at a silently builtin-only table —
|
|
62
|
+
# unreachable servers additionally warn via Manager#start_server.
|
|
63
|
+
def print_mcp_tools
|
|
64
|
+
return unless MCP.enabled?
|
|
65
|
+
|
|
66
|
+
ui = Rubino.ui
|
|
67
|
+
ui.blank_line
|
|
68
|
+
ui.info("MCP Tools (experimental):")
|
|
69
|
+
MCP.boot!
|
|
70
|
+
|
|
71
|
+
mcp_tools = Tools::Registry.all.grep(MCP::MCPToolWrapper)
|
|
72
|
+
if mcp_tools.empty?
|
|
73
|
+
ui.warning("mcp.servers configured, but no MCP tools loaded")
|
|
74
|
+
else
|
|
75
|
+
rows = mcp_tools.map { |t| [t.name, t.server_name] }.sort
|
|
76
|
+
ui.table(headers: %w[Tool Server], rows: rows)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module CLI
|
|
5
|
+
# The interactive folder-trust checkpoint. Asks ONCE per directory, before
|
|
6
|
+
# that directory's AGENTS.md / project context + .rubino/skills are honored,
|
|
7
|
+
# and remembers the answer in Rubino::Trust so it's never re-asked.
|
|
8
|
+
#
|
|
9
|
+
# Modelled on VS Code Workspace Trust + Claude Code's trust dialog. Declining
|
|
10
|
+
# is non-destructive (VS Code "Restricted Mode"): the session still runs, it
|
|
11
|
+
# just runs WITHOUT that directory's project context/skills (the assembler
|
|
12
|
+
# consults Rubino::Trust.trusted? before injecting them).
|
|
13
|
+
#
|
|
14
|
+
# Skipped entirely — no prompt, treated as allowed for the duration — when:
|
|
15
|
+
# - the dir is already trusted,
|
|
16
|
+
# - the dir has nothing to gate (no context file, no .rubino/skills),
|
|
17
|
+
# - --ignore-rules was passed (project context is off regardless), or
|
|
18
|
+
# - the run is non-interactive (-q / no TTY): we never block automation.
|
|
19
|
+
class TrustGate
|
|
20
|
+
def initialize(ui: nil, interactive: true, ignore_rules: false)
|
|
21
|
+
@ui = ui || Rubino.ui
|
|
22
|
+
@interactive = interactive
|
|
23
|
+
@ignore_rules = ignore_rules
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Ensures +dir+ has a trust decision. Returns true when the directory's
|
|
27
|
+
# project context/skills may be loaded, false when it must run in
|
|
28
|
+
# restricted mode. Prompts at most once, then remembers a "yes".
|
|
29
|
+
def ensure_trust(dir)
|
|
30
|
+
return true if Rubino::Trust.trusted?(dir)
|
|
31
|
+
return true if @ignore_rules # context already suppressed
|
|
32
|
+
return true unless gateworthy?(dir) # nothing to gate → no ceremony
|
|
33
|
+
|
|
34
|
+
# Non-interactive: never block. We also do NOT remember it (the user
|
|
35
|
+
# never vouched), so context stays withheld this run — Restricted Mode
|
|
36
|
+
# by default for automation, matching VS Code's headless behaviour.
|
|
37
|
+
return false unless @interactive
|
|
38
|
+
|
|
39
|
+
prompt(dir)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Asks the one-time question and records the answer. Default is No.
|
|
45
|
+
def prompt(dir)
|
|
46
|
+
@ui.blank_line if @ui.respond_to?(:blank_line)
|
|
47
|
+
@ui.info("▸ Starting in #{dir} — its AGENTS.md and project skills will shape the agent.")
|
|
48
|
+
answer = @ui.ask(" Trust this folder? [y/N] ").to_s.strip.downcase
|
|
49
|
+
|
|
50
|
+
if answer.start_with?("y")
|
|
51
|
+
Rubino::Trust.remember(dir)
|
|
52
|
+
@ui.success("Trusted #{dir} — loading its project context and skills.") if @ui.respond_to?(:success)
|
|
53
|
+
true
|
|
54
|
+
else
|
|
55
|
+
@ui.info("Running in restricted mode — #{dir}'s AGENTS.md and skills will NOT be loaded.")
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A directory is only worth a trust prompt when it actually ships
|
|
61
|
+
# something rubino auto-injects: a project-context file or a skills dir.
|
|
62
|
+
# An empty scratch dir gets no prompt — there's nothing to be steered by.
|
|
63
|
+
def gateworthy?(dir)
|
|
64
|
+
Context::FileDiscovery.new(base_path: dir).discover_files.any? ||
|
|
65
|
+
File.directory?(File.join(dir, Skills::PromptIndex::DEFAULT_SKILL_DIR))
|
|
66
|
+
rescue StandardError
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Commands
|
|
5
|
+
# Single source of truth for built-in slash command names + descriptions.
|
|
6
|
+
# Referenced by the CLI UI for tab-completion and by the Executor for
|
|
7
|
+
# `/help` and the unknown-command "Available" list (so both stay in sync —
|
|
8
|
+
# previously `/help` omitted /quit and /mode, L7).
|
|
9
|
+
module BuiltIns
|
|
10
|
+
# Ordered: name => one-line description shown by `/help`.
|
|
11
|
+
DESCRIPTIONS = {
|
|
12
|
+
"/status" => "Overview: model, mode, session, memory, background work",
|
|
13
|
+
"/sessions" => "List recent sessions; resume, show, or delete one (--all lifts the cap)",
|
|
14
|
+
"/new" => "Start a fresh session (the current one is left intact)",
|
|
15
|
+
"/clear" => "Alias for /new — start a fresh session",
|
|
16
|
+
"/probe" => "Ask an ephemeral side-question (not saved); tip: start a line with '? '",
|
|
17
|
+
"/queued" => "Queue a message to run after the current turn (Alt+Enter does the same)",
|
|
18
|
+
"/branch" => "Fork the current session into a new one and switch into it",
|
|
19
|
+
"/compact" => "Compact the context now: older turns become a summary",
|
|
20
|
+
"/export" => "Write the session transcript as markdown (/export [path])",
|
|
21
|
+
"/memory" => "Inspect/search/forget what the agent remembers (show ID, backend, --all)",
|
|
22
|
+
"/agents" => "List background subagents; steer/probe a running one, or view output",
|
|
23
|
+
"/tasks" => "Alias for /agents",
|
|
24
|
+
"/reply" => "Answer a subagent that is blocked waiting on you (ask_parent)",
|
|
25
|
+
"/jobs" => "List the background job queue (status counts); /jobs <id> for detail",
|
|
26
|
+
"/skills" => "List skills; activate one ('none' clears), or enable/disable NAME",
|
|
27
|
+
"/mcp" => "List MCP servers and their tools; restart or disable one",
|
|
28
|
+
"/add-dir" => "Add an extra allowed workspace directory (write/edit can reach it)",
|
|
29
|
+
"/dirs" => "List the current workspace roots",
|
|
30
|
+
"/config" => "Read or set configuration (/config <key> [value]; 'show' = full view)",
|
|
31
|
+
"/model" => "Show or switch the model for this session (/model <name>)",
|
|
32
|
+
"/mode" => "Show or switch mode (default | plan | yolo)",
|
|
33
|
+
"/reasoning" => "Show or switch how reasoning is shown (hidden | collapsed | full)",
|
|
34
|
+
"/think" => "Show or switch thinking effort (off | low | medium | high)",
|
|
35
|
+
"/commands" => "List custom commands (and how to make them)",
|
|
36
|
+
"/help" => "Show this help",
|
|
37
|
+
"/paste" => "Attach an image from the clipboard",
|
|
38
|
+
"/clear-images" => "Drop pending image attachments",
|
|
39
|
+
"/exit" => "End session",
|
|
40
|
+
"/quit" => "End session"
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
NAMES = DESCRIPTIONS.keys.freeze
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module Commands
|
|
7
|
+
# Represents a custom slash command loaded from a Markdown file.
|
|
8
|
+
# Supports $ARGUMENTS, $1-$9 positional params, @file refs.
|
|
9
|
+
#
|
|
10
|
+
# Shell injection via !`command` is opt-in and disabled by default.
|
|
11
|
+
# Enable it by setting commands.shell_injection_enabled: true in your
|
|
12
|
+
# configuration — only do so in trusted, controlled environments.
|
|
13
|
+
class Command
|
|
14
|
+
attr_reader :name, :description, :agent, :model, :path
|
|
15
|
+
|
|
16
|
+
def initialize(path:)
|
|
17
|
+
@path = path
|
|
18
|
+
@metadata = {}
|
|
19
|
+
@template = nil
|
|
20
|
+
parse!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Renders the command prompt with given arguments.
|
|
24
|
+
def render(arguments = "")
|
|
25
|
+
prompt = template.dup
|
|
26
|
+
|
|
27
|
+
substitute_arguments!(prompt, arguments)
|
|
28
|
+
process_shell_injections!(prompt)
|
|
29
|
+
process_file_references!(prompt)
|
|
30
|
+
|
|
31
|
+
prompt.strip
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the raw template content.
|
|
35
|
+
def template
|
|
36
|
+
@template ||= load_template
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Replace $ARGUMENTS and positional $1..$9 params.
|
|
42
|
+
def substitute_arguments!(prompt, arguments)
|
|
43
|
+
prompt.gsub!("$ARGUMENTS", arguments)
|
|
44
|
+
|
|
45
|
+
args = arguments.split(/\s+/)
|
|
46
|
+
(1..9).each do |i|
|
|
47
|
+
prompt.gsub!("$#{i}", args[i - 1] || "")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Process !`command` shell injections — only when explicitly enabled.
|
|
52
|
+
def process_shell_injections!(prompt)
|
|
53
|
+
return unless shell_injection_enabled?
|
|
54
|
+
|
|
55
|
+
prompt.gsub!(/!`([^`]+)`/) do
|
|
56
|
+
command = Regexp.last_match(1)
|
|
57
|
+
`#{command} 2>&1`.strip
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Replace @path/to/file references with file content.
|
|
62
|
+
def process_file_references!(prompt)
|
|
63
|
+
prompt.gsub!(%r{@([\w/._-]+)}) do
|
|
64
|
+
file_path = Regexp.last_match(1)
|
|
65
|
+
expanded = File.expand_path(file_path)
|
|
66
|
+
if File.exist?(expanded)
|
|
67
|
+
File.read(expanded)
|
|
68
|
+
else
|
|
69
|
+
"@#{file_path} (file not found)"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def shell_injection_enabled?
|
|
75
|
+
Rubino.configuration.dig("commands", "shell_injection_enabled") == true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse!
|
|
79
|
+
raw = File.read(@path)
|
|
80
|
+
|
|
81
|
+
if raw.start_with?("---")
|
|
82
|
+
parts = raw.split("---", 3)
|
|
83
|
+
if parts.size >= 3
|
|
84
|
+
begin
|
|
85
|
+
@metadata = YAML.safe_load(parts[1], permitted_classes: [Symbol]) || {}
|
|
86
|
+
rescue Psych::SyntaxError => e
|
|
87
|
+
warn "rubino: skipping malformed frontmatter in #{@path} " \
|
|
88
|
+
"(line #{e.line}: #{e.problem}); treating whole file as template"
|
|
89
|
+
@metadata = {}
|
|
90
|
+
@template = raw
|
|
91
|
+
end
|
|
92
|
+
unless @metadata.is_a?(Hash)
|
|
93
|
+
warn "rubino: ignoring non-Hash frontmatter in #{@path}; treating whole file as template"
|
|
94
|
+
@metadata = {}
|
|
95
|
+
@template = raw
|
|
96
|
+
end
|
|
97
|
+
@template ||= parts[2].strip
|
|
98
|
+
else
|
|
99
|
+
@template = raw
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
@template = raw
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@name = (@metadata["name"] || File.basename(@path, ".md")).to_s
|
|
106
|
+
@description = @metadata["description"] || ""
|
|
107
|
+
@agent = @metadata["agent"]
|
|
108
|
+
@model = @metadata["model"]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def load_template
|
|
112
|
+
@template
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|