octo-agent 0.11.2
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/.clacky/skills/commit/SKILL.md +423 -0
- data/.clacky/skills/gem-release/SKILL.md +199 -0
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/.clacky/skills/oss-upload/SKILL.md +47 -0
- data/.octorules +106 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +76 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +92 -0
- data/Dockerfile +28 -0
- data/LICENSE.txt +22 -0
- data/POSITIONING.md +46 -0
- data/README.md +134 -0
- data/README_CN.md +134 -0
- data/Rakefile +34 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/bin/octo +7 -0
- data/docs/agent-first-ui-design.md +77 -0
- data/docs/billing-system.md +318 -0
- data/docs/channel-architecture.md +235 -0
- data/docs/engineering-article.md +343 -0
- data/docs/session-skill-invocation.md +69 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/ui2-architecture.md +124 -0
- data/homebrew/README.md +96 -0
- data/homebrew/openocto.rb +24 -0
- data/lib/octo/agent/hook_manager.rb +61 -0
- data/lib/octo/agent/llm_caller.rb +800 -0
- data/lib/octo/agent/memory_updater.rb +246 -0
- data/lib/octo/agent/message_compressor.rb +225 -0
- data/lib/octo/agent/message_compressor_helper.rb +869 -0
- data/lib/octo/agent/next_message_suggester.rb +215 -0
- data/lib/octo/agent/session_serializer.rb +685 -0
- data/lib/octo/agent/skill_auto_creator.rb +114 -0
- data/lib/octo/agent/skill_evolution.rb +61 -0
- data/lib/octo/agent/skill_manager.rb +466 -0
- data/lib/octo/agent/skill_reflector.rb +89 -0
- data/lib/octo/agent/system_prompt_builder.rb +101 -0
- data/lib/octo/agent/time_machine.rb +214 -0
- data/lib/octo/agent/tool_executor.rb +454 -0
- data/lib/octo/agent/tool_registry.rb +150 -0
- data/lib/octo/agent.rb +2180 -0
- data/lib/octo/agent_config.rb +989 -0
- data/lib/octo/agent_profile.rb +112 -0
- data/lib/octo/anthropic_stream_aggregator.rb +137 -0
- data/lib/octo/background_task_registry.rb +324 -0
- data/lib/octo/banner.rb +34 -0
- data/lib/octo/bedrock_stream_aggregator.rb +137 -0
- data/lib/octo/block_font.rb +331 -0
- data/lib/octo/cli.rb +968 -0
- data/lib/octo/client.rb +623 -0
- data/lib/octo/default_agents/SOUL.md +3 -0
- data/lib/octo/default_agents/USER.md +1 -0
- data/lib/octo/default_agents/base_prompt.md +66 -0
- data/lib/octo/default_agents/coding/profile.yml +2 -0
- data/lib/octo/default_agents/coding/system_prompt.md +67 -0
- data/lib/octo/default_agents/general/profile.yml +2 -0
- data/lib/octo/default_agents/general/system_prompt.md +16 -0
- data/lib/octo/default_parsers/doc_parser.rb +69 -0
- data/lib/octo/default_parsers/docx_parser.rb +188 -0
- data/lib/octo/default_parsers/pdf_parser.rb +120 -0
- data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/octo/default_parsers/pptx_parser.rb +140 -0
- data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
- data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
- data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
- data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
- data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
- data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
- data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
- data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
- data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/octo/default_skills/onboard/SKILL.md +578 -0
- data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
- data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
- data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
- data/lib/octo/default_skills/personal-website/publish.rb +235 -0
- data/lib/octo/default_skills/product-help/SKILL.md +123 -0
- data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
- data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
- data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
- data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
- data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
- data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
- data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
- data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
- data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
- data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
- data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
- data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
- data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
- data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
- data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
- data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
- data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
- data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
- data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
- data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
- data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
- data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
- data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/octo/idle_compression_timer.rb +115 -0
- data/lib/octo/json_ui_controller.rb +204 -0
- data/lib/octo/message_format/anthropic.rb +409 -0
- data/lib/octo/message_format/bedrock.rb +361 -0
- data/lib/octo/message_format/open_ai.rb +222 -0
- data/lib/octo/message_history.rb +373 -0
- data/lib/octo/openai_stream_aggregator.rb +130 -0
- data/lib/octo/plain_ui_controller.rb +166 -0
- data/lib/octo/providers.rb +534 -0
- data/lib/octo/server/browser_manager.rb +397 -0
- data/lib/octo/server/channel/adapters/base.rb +82 -0
- data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
- data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
- data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
- data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
- data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
- data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
- data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
- data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
- data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
- data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
- data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
- data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
- data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
- data/lib/octo/server/channel/channel_config.rb +178 -0
- data/lib/octo/server/channel/channel_manager.rb +468 -0
- data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
- data/lib/octo/server/channel.rb +33 -0
- data/lib/octo/server/discover.rb +77 -0
- data/lib/octo/server/epipe_safe_io.rb +105 -0
- data/lib/octo/server/http_server.rb +3554 -0
- data/lib/octo/server/scheduler.rb +317 -0
- data/lib/octo/server/server_master.rb +325 -0
- data/lib/octo/server/session_registry.rb +431 -0
- data/lib/octo/server/web_ui_controller.rb +487 -0
- data/lib/octo/session_manager.rb +385 -0
- data/lib/octo/skill.rb +466 -0
- data/lib/octo/skill_loader.rb +328 -0
- data/lib/octo/tools/base.rb +118 -0
- data/lib/octo/tools/browser.rb +625 -0
- data/lib/octo/tools/edit.rb +165 -0
- data/lib/octo/tools/file_reader.rb +549 -0
- data/lib/octo/tools/glob.rb +162 -0
- data/lib/octo/tools/grep.rb +356 -0
- data/lib/octo/tools/invoke_skill.rb +96 -0
- data/lib/octo/tools/list_tasks.rb +54 -0
- data/lib/octo/tools/redo_task.rb +41 -0
- data/lib/octo/tools/request_user_feedback.rb +84 -0
- data/lib/octo/tools/security.rb +333 -0
- data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
- data/lib/octo/tools/terminal/persistent_session.rb +268 -0
- data/lib/octo/tools/terminal/safe_rm.sh +106 -0
- data/lib/octo/tools/terminal/session_manager.rb +213 -0
- data/lib/octo/tools/terminal.rb +1828 -0
- data/lib/octo/tools/todo_manager.rb +374 -0
- data/lib/octo/tools/trash_manager.rb +388 -0
- data/lib/octo/tools/undo_task.rb +35 -0
- data/lib/octo/tools/web_fetch.rb +242 -0
- data/lib/octo/tools/web_search.rb +260 -0
- data/lib/octo/tools/write.rb +77 -0
- data/lib/octo/ui2/block_font.rb +10 -0
- data/lib/octo/ui2/components/base_component.rb +163 -0
- data/lib/octo/ui2/components/command_suggestions.rb +290 -0
- data/lib/octo/ui2/components/common_component.rb +96 -0
- data/lib/octo/ui2/components/inline_input.rb +226 -0
- data/lib/octo/ui2/components/input_area.rb +1338 -0
- data/lib/octo/ui2/components/message_component.rb +99 -0
- data/lib/octo/ui2/components/modal_component.rb +419 -0
- data/lib/octo/ui2/components/todo_area.rb +149 -0
- data/lib/octo/ui2/components/tool_component.rb +107 -0
- data/lib/octo/ui2/components/welcome_banner.rb +139 -0
- data/lib/octo/ui2/layout_manager.rb +807 -0
- data/lib/octo/ui2/line_editor.rb +363 -0
- data/lib/octo/ui2/markdown_renderer.rb +100 -0
- data/lib/octo/ui2/output_buffer.rb +370 -0
- data/lib/octo/ui2/progress_handle.rb +362 -0
- data/lib/octo/ui2/progress_indicator.rb +55 -0
- data/lib/octo/ui2/screen_buffer.rb +273 -0
- data/lib/octo/ui2/terminal_detector.rb +119 -0
- data/lib/octo/ui2/theme_manager.rb +85 -0
- data/lib/octo/ui2/themes/base_theme.rb +105 -0
- data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
- data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
- data/lib/octo/ui2/thinking_verbs.rb +26 -0
- data/lib/octo/ui2/ui_controller.rb +1625 -0
- data/lib/octo/ui2/view_renderer.rb +177 -0
- data/lib/octo/ui2.rb +40 -0
- data/lib/octo/ui_interface.rb +154 -0
- data/lib/octo/utils/arguments_parser.rb +191 -0
- data/lib/octo/utils/browser_detector.rb +195 -0
- data/lib/octo/utils/encoding.rb +92 -0
- data/lib/octo/utils/environment_detector.rb +140 -0
- data/lib/octo/utils/file_ignore_helper.rb +170 -0
- data/lib/octo/utils/file_processor.rb +601 -0
- data/lib/octo/utils/gitignore_parser.rb +154 -0
- data/lib/octo/utils/limit_stack.rb +152 -0
- data/lib/octo/utils/logger.rb +124 -0
- data/lib/octo/utils/login_shell.rb +72 -0
- data/lib/octo/utils/model_pricing.rb +646 -0
- data/lib/octo/utils/parser_manager.rb +165 -0
- data/lib/octo/utils/path_helper.rb +15 -0
- data/lib/octo/utils/scripts_manager.rb +59 -0
- data/lib/octo/utils/string_matcher.rb +158 -0
- data/lib/octo/utils/trash_directory.rb +112 -0
- data/lib/octo/utils/workspace_rules.rb +46 -0
- data/lib/octo/version.rb +5 -0
- data/lib/octo/web/app.css +7141 -0
- data/lib/octo/web/app.js +543 -0
- data/lib/octo/web/apple-touch-icon.png +0 -0
- data/lib/octo/web/auth.js +150 -0
- data/lib/octo/web/channels.js +276 -0
- data/lib/octo/web/datepicker.js +205 -0
- data/lib/octo/web/favicon.png +0 -0
- data/lib/octo/web/i18n.js +1073 -0
- data/lib/octo/web/icon-512.png +0 -0
- data/lib/octo/web/icon-dark.svg +25 -0
- data/lib/octo/web/icon.svg +29 -0
- data/lib/octo/web/index.html +871 -0
- data/lib/octo/web/marked.min.js +69 -0
- data/lib/octo/web/onboard.js +491 -0
- data/lib/octo/web/profile.js +442 -0
- data/lib/octo/web/sessions.js +4421 -0
- data/lib/octo/web/settings.js +913 -0
- data/lib/octo/web/sidebar.js +32 -0
- data/lib/octo/web/skills.js +885 -0
- data/lib/octo/web/tasks.js +297 -0
- data/lib/octo/web/theme.js +105 -0
- data/lib/octo/web/trash.js +343 -0
- data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
- data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/katex.min.css +1 -0
- data/lib/octo/web/vendor/katex/katex.min.js +1 -0
- data/lib/octo/web/version.js +449 -0
- data/lib/octo/web/weixin-qr.html +209 -0
- data/lib/octo/web/ws-dispatcher.js +357 -0
- data/lib/octo/web/ws.js +128 -0
- data/lib/octo.rb +145 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +56 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +157 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +174 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.ps1 +532 -0
- data/scripts/install.sh +567 -0
- data/scripts/install_browser.sh +479 -0
- data/scripts/install_full.sh +838 -0
- data/scripts/install_rails_deps.sh +746 -0
- data/scripts/install_system_deps.sh +518 -0
- data/scripts/uninstall.sh +287 -0
- data/sig/octo.rbs +4 -0
- metadata +614 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "octo"
|
|
6
|
+
|
|
7
|
+
module Octo
|
|
8
|
+
# Loader and registry for skills.
|
|
9
|
+
# Discovers skills from multiple locations and provides lookup functionality.
|
|
10
|
+
class SkillLoader
|
|
11
|
+
# Skill discovery locations (in priority order: lower index = lower priority)
|
|
12
|
+
LOCATIONS = [
|
|
13
|
+
:default, # gem's built-in default skills (lowest priority)
|
|
14
|
+
:global_octo, # ~/.octo/skills/
|
|
15
|
+
:project_octo # .octo/skills/ (highest priority)
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
# Initialize the skill loader and automatically load all skills
|
|
19
|
+
# @param working_dir [String, nil] Current working directory for project-level discovery.
|
|
20
|
+
# When nil, project-level skills (.octo/skills/) are not loaded,
|
|
21
|
+
# making the loader project-agnostic (used by WebUI server).
|
|
22
|
+
def initialize(working_dir:)
|
|
23
|
+
@working_dir = working_dir
|
|
24
|
+
@skills = {} # Map identifier -> Skill
|
|
25
|
+
@skills_by_command = {} # Map slash_command -> Skill
|
|
26
|
+
@errors = [] # Store loading errors
|
|
27
|
+
@loaded_from = {} # Track which location each skill was loaded from
|
|
28
|
+
|
|
29
|
+
load_all
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Load all skills from configured locations
|
|
33
|
+
# Clears previously loaded skills before loading to ensure idempotency
|
|
34
|
+
# @return [Array<Skill>] Loaded skills
|
|
35
|
+
def load_all
|
|
36
|
+
# Clear existing skills to ensure idempotent reloading
|
|
37
|
+
clear
|
|
38
|
+
|
|
39
|
+
load_default_skills
|
|
40
|
+
load_global_octo_skills
|
|
41
|
+
|
|
42
|
+
# Only load project-level skills when working_dir is explicitly provided.
|
|
43
|
+
# When nil (e.g. WebUI server mode), skip project skills to keep the loader
|
|
44
|
+
# project-agnostic and only expose global skills.
|
|
45
|
+
if @working_dir
|
|
46
|
+
load_project_octo_skills
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
all_skills
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Load skills from ~/.octo/skills/ (user global)
|
|
53
|
+
# @return [Array<Skill>]
|
|
54
|
+
def load_global_octo_skills
|
|
55
|
+
global_octo_dir = Pathname.new(ENV.fetch("HOME", "~")).join(".octo", "skills")
|
|
56
|
+
load_skills_from_directory(global_octo_dir, :global_octo)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Load skills from .octo/skills/ (project-level, highest priority)
|
|
60
|
+
# @return [Array<Skill>]
|
|
61
|
+
def load_project_octo_skills
|
|
62
|
+
project_octo_dir = Pathname.new(@working_dir).join(".octo", "skills")
|
|
63
|
+
load_skills_from_directory(project_octo_dir, :project_octo)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get all loaded skills
|
|
67
|
+
# @return [Array<Skill>]
|
|
68
|
+
def all_skills
|
|
69
|
+
@skills.values
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get a skill by its identifier
|
|
73
|
+
# @param identifier [String] Skill name or directory name
|
|
74
|
+
# @return [Skill, nil]
|
|
75
|
+
def [](identifier)
|
|
76
|
+
@skills[identifier]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Find a skill by its slash command
|
|
80
|
+
# @param command [String] e.g., "/explain-code"
|
|
81
|
+
# @return [Skill, nil]
|
|
82
|
+
def find_by_command(command)
|
|
83
|
+
@skills_by_command[command]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Find a skill by its name (identifier)
|
|
87
|
+
# @param name [String] Skill identifier (e.g., "code-explorer", "pptx")
|
|
88
|
+
# @return [Skill, nil]
|
|
89
|
+
def find_by_name(name)
|
|
90
|
+
@skills[name]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get skills that can be invoked by user
|
|
94
|
+
# @return [Array<Skill>]
|
|
95
|
+
def user_invocable_skills
|
|
96
|
+
all_skills.select(&:user_invocable?)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get the count of loaded skills
|
|
100
|
+
# @return [Integer]
|
|
101
|
+
def count
|
|
102
|
+
@skills.size
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Get loading errors
|
|
106
|
+
# @return [Array<String>]
|
|
107
|
+
def errors
|
|
108
|
+
@errors.dup
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get the source location for each loaded skill
|
|
112
|
+
# @return [Hash{String => Symbol}] Map of skill identifier to source location
|
|
113
|
+
def loaded_from
|
|
114
|
+
@loaded_from.dup
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Clear loaded skills and errors
|
|
118
|
+
def clear
|
|
119
|
+
@skills.clear
|
|
120
|
+
@skills_by_command.clear
|
|
121
|
+
@loaded_from.clear
|
|
122
|
+
@errors.clear
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Create a new skill directory and SKILL.md file
|
|
126
|
+
# @param name [String] Skill name (will be used for directory and slash command)
|
|
127
|
+
# @param content [String] Skill content (SKILL.md body)
|
|
128
|
+
# @param description [String] Skill description
|
|
129
|
+
# @param location [Symbol] Where to create: :global or :project
|
|
130
|
+
# @return [Skill] The created skill
|
|
131
|
+
def create_skill(name, content, description = nil, location: :global)
|
|
132
|
+
# Validate name
|
|
133
|
+
unless name.match?(/^[a-z0-9][a-z0-9-]*$/)
|
|
134
|
+
raise Octo::AgentError,
|
|
135
|
+
"Invalid skill name '#{name}'. Use lowercase letters, numbers, and hyphens only."
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Determine directory path
|
|
139
|
+
skill_dir = case location
|
|
140
|
+
when :global
|
|
141
|
+
Pathname.new(ENV.fetch("HOME", "~")).join(".octo", "skills", name)
|
|
142
|
+
when :project
|
|
143
|
+
Pathname.new(@working_dir).join(".octo", "skills", name)
|
|
144
|
+
else
|
|
145
|
+
raise Octo::AgentError, "Unknown skill location: #{location}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Create directory if it doesn't exist
|
|
149
|
+
FileUtils.mkdir_p(skill_dir)
|
|
150
|
+
|
|
151
|
+
# Build frontmatter
|
|
152
|
+
frontmatter = { "name" => name, "description" => description }
|
|
153
|
+
|
|
154
|
+
# Write SKILL.md
|
|
155
|
+
skill_content = build_skill_content(frontmatter, content)
|
|
156
|
+
skill_file = skill_dir.join("SKILL.md")
|
|
157
|
+
skill_file.write(skill_content)
|
|
158
|
+
|
|
159
|
+
# Load the newly created skill
|
|
160
|
+
source_type = case location
|
|
161
|
+
when :global then :global_octo
|
|
162
|
+
when :project then :project_octo
|
|
163
|
+
else :global_octo
|
|
164
|
+
end
|
|
165
|
+
load_single_skill(skill_dir, skill_dir, name, source_type)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Toggle a skill's disable-model-invocation field in its SKILL.md.
|
|
169
|
+
# System skills (source: :default) cannot be toggled — raises AgentError.
|
|
170
|
+
# @param name [String] Skill identifier
|
|
171
|
+
# @param enabled [Boolean] true = enable, false = disable
|
|
172
|
+
# @return [Skill] The reloaded skill
|
|
173
|
+
def toggle_skill(name, enabled:)
|
|
174
|
+
skill = @skills[name]
|
|
175
|
+
raise Octo::AgentError, "Skill not found: #{name}" unless skill
|
|
176
|
+
raise Octo::AgentError, "Cannot toggle system skill: #{name}" if @loaded_from[name] == :default
|
|
177
|
+
|
|
178
|
+
skill_file = skill.directory.join("SKILL.md")
|
|
179
|
+
fm = (skill.frontmatter || {}).dup
|
|
180
|
+
|
|
181
|
+
if enabled
|
|
182
|
+
fm["disable-model-invocation"] = false
|
|
183
|
+
else
|
|
184
|
+
fm["disable-model-invocation"] = true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
skill_file.write(build_skill_content(fm, skill.content))
|
|
188
|
+
|
|
189
|
+
# Reload into registry
|
|
190
|
+
reloaded = Skill.new(skill.directory, source_path: skill.source_path)
|
|
191
|
+
@skills[reloaded.identifier] = reloaded
|
|
192
|
+
@skills_by_command[reloaded.slash_command] = reloaded
|
|
193
|
+
reloaded
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Delete a skill
|
|
197
|
+
# @param name [String] Skill name
|
|
198
|
+
# @return [Boolean] True if deleted, false if not found
|
|
199
|
+
def delete_skill(name)
|
|
200
|
+
skill = @skills[name]
|
|
201
|
+
return false unless skill
|
|
202
|
+
|
|
203
|
+
# Remove from registry
|
|
204
|
+
@skills.delete(name)
|
|
205
|
+
@skills_by_command.delete(skill.slash_command)
|
|
206
|
+
|
|
207
|
+
# Delete directory
|
|
208
|
+
FileUtils.rm_rf(skill.directory)
|
|
209
|
+
|
|
210
|
+
true
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def load_skills_from_directory(dir, source_type)
|
|
215
|
+
return [] unless dir.exist?
|
|
216
|
+
|
|
217
|
+
source_path = case source_type
|
|
218
|
+
when :global_octo
|
|
219
|
+
Pathname.new(ENV.fetch("HOME", "~")).join(".octo")
|
|
220
|
+
when :project_octo
|
|
221
|
+
Pathname.new(@working_dir)
|
|
222
|
+
else
|
|
223
|
+
dir
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
skills = []
|
|
227
|
+
dir.children.select(&:directory?).each do |entry|
|
|
228
|
+
if entry.join("SKILL.md").exist?
|
|
229
|
+
# Direct skill directory
|
|
230
|
+
skill = load_single_skill(entry, source_path, entry.basename.to_s, source_type)
|
|
231
|
+
skills << skill if skill
|
|
232
|
+
else
|
|
233
|
+
# Treat as a category directory — scan one level deeper for skills.
|
|
234
|
+
# This allows grouping skills under ~/.octo/skills/<category>/<skill>/SKILL.md
|
|
235
|
+
# (e.g. openclaw-imports/my-skill/SKILL.md) without changing the loader contract.
|
|
236
|
+
entry.children.select(&:directory?).each do |skill_dir|
|
|
237
|
+
next unless skill_dir.join("SKILL.md").exist?
|
|
238
|
+
|
|
239
|
+
skill = load_single_skill(skill_dir, source_path, skill_dir.basename.to_s, source_type)
|
|
240
|
+
skills << skill if skill
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
skills
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
private def load_single_skill(skill_dir, source_path, skill_name, source_type)
|
|
248
|
+
skill = Skill.new(skill_dir, source_path: source_path)
|
|
249
|
+
register_skill(skill, source: source_type)
|
|
250
|
+
skill
|
|
251
|
+
rescue Octo::AgentError => e
|
|
252
|
+
@errors << "Error loading skill '#{skill_name}' from #{skill_dir}: #{e.message}"
|
|
253
|
+
nil
|
|
254
|
+
rescue StandardError => e
|
|
255
|
+
@errors << "Unexpected error loading skill '#{skill_name}' from #{skill_dir}: #{e.message}"
|
|
256
|
+
nil
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Register a skill into the internal lookup tables.
|
|
260
|
+
# - Always adds to @skills (by identifier) so the skill is discoverable in the UI.
|
|
261
|
+
# - Skips @skills_by_command registration when the skill is invalid (no valid slug
|
|
262
|
+
# to form a slash command from).
|
|
263
|
+
# @param skill [Skill]
|
|
264
|
+
# @param source [Symbol] one of :default, :global_octo, :project_octo
|
|
265
|
+
# @return [Skill, nil] nil when the skill was rejected (duplicate/limit)
|
|
266
|
+
private def register_skill(skill, source:)
|
|
267
|
+
id = skill.identifier
|
|
268
|
+
priority_order = %i[default global_octo project_octo]
|
|
269
|
+
|
|
270
|
+
# --- duplicate check ---
|
|
271
|
+
if (existing = @skills[id])
|
|
272
|
+
existing_source = @loaded_from[id]
|
|
273
|
+
if priority_order.index(source) > priority_order.index(existing_source)
|
|
274
|
+
# Incoming skill has higher priority — evict the existing one
|
|
275
|
+
@skills.delete(existing.identifier)
|
|
276
|
+
@skills_by_command.delete(existing.slash_command)
|
|
277
|
+
@loaded_from.delete(existing.identifier)
|
|
278
|
+
else
|
|
279
|
+
@errors << "Skipping duplicate skill '#{id}' (lower priority) from #{skill.directory}"
|
|
280
|
+
return nil
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Register in main skills hash
|
|
285
|
+
@skills[id] = skill
|
|
286
|
+
@loaded_from[id] = source
|
|
287
|
+
skill.source = source
|
|
288
|
+
|
|
289
|
+
# Invalid skills have no usable slug — skip slash command registration but
|
|
290
|
+
# still keep them in @skills so they appear (greyed-out) in the UI.
|
|
291
|
+
unless skill.invalid?
|
|
292
|
+
@skills_by_command[skill.slash_command] = skill
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
skill
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def build_skill_content(frontmatter, content)
|
|
299
|
+
yaml = frontmatter
|
|
300
|
+
.reject { |_, v| v.nil? || v.to_s.empty? }
|
|
301
|
+
.to_yaml(line_width: 80)
|
|
302
|
+
|
|
303
|
+
"---\n#{yaml}---\n\n#{content}"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Load default skills from gem's default_skills directory
|
|
307
|
+
private def load_default_skills
|
|
308
|
+
# Get the gem's lib directory
|
|
309
|
+
gem_lib_dir = File.expand_path("../", __dir__)
|
|
310
|
+
default_skills_dir = File.join(gem_lib_dir, "octo", "default_skills")
|
|
311
|
+
|
|
312
|
+
return unless Dir.exist?(default_skills_dir)
|
|
313
|
+
|
|
314
|
+
# Load each skill directory
|
|
315
|
+
Dir.glob(File.join(default_skills_dir, "*/SKILL.md")).each do |skill_file|
|
|
316
|
+
skill_dir = File.dirname(skill_file)
|
|
317
|
+
skill_name = File.basename(skill_dir)
|
|
318
|
+
|
|
319
|
+
begin
|
|
320
|
+
skill = Skill.new(Pathname.new(skill_dir))
|
|
321
|
+
register_skill(skill, source: :default)
|
|
322
|
+
rescue StandardError => e
|
|
323
|
+
@errors << "Failed to load default skill #{skill_name}: #{e.message}"
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
module Tools
|
|
5
|
+
class Base
|
|
6
|
+
class << self
|
|
7
|
+
attr_accessor :tool_name, :tool_description, :tool_parameters, :tool_category
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def name
|
|
11
|
+
self.class.tool_name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def description
|
|
15
|
+
self.class.tool_description
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parameters
|
|
19
|
+
self.class.tool_parameters
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def category
|
|
23
|
+
self.class.tool_category || "general"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Execute the tool - must be implemented by subclasses
|
|
27
|
+
def execute(**_args)
|
|
28
|
+
raise NotImplementedError, "#{self.class.name} must implement #execute"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Expand ~ to home directory only if path starts with ~
|
|
32
|
+
# Relative paths are resolved against working_dir if provided
|
|
33
|
+
# @param path [String, nil] The path to expand
|
|
34
|
+
# @param working_dir [String, nil] The working directory to resolve relative paths against
|
|
35
|
+
# @return [String, nil] The expanded path, or original if no ~ present
|
|
36
|
+
private def expand_path(path, working_dir: nil)
|
|
37
|
+
return path if path.nil? || path.strip.empty?
|
|
38
|
+
return File.expand_path(path) if path.start_with?("~")
|
|
39
|
+
return File.expand_path(path, working_dir) if working_dir && !path.start_with?("/")
|
|
40
|
+
# Always resolve relative paths to absolute (even without working_dir), so callers
|
|
41
|
+
# never receive a bare "." that resolves against the process cwd unexpectedly.
|
|
42
|
+
return File.expand_path(path) unless path.start_with?("/")
|
|
43
|
+
|
|
44
|
+
path
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Format tool call for display - can be overridden by subclasses
|
|
48
|
+
# @param args [Hash] The arguments passed to the tool
|
|
49
|
+
# @return [String] Formatted call description (e.g., "Read(file.rb)")
|
|
50
|
+
def format_call(args)
|
|
51
|
+
"#{name}(...)"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Format tool result for display - can be overridden by subclasses
|
|
55
|
+
# @param result [Object] The result returned by execute
|
|
56
|
+
# @return [String] Formatted result summary (e.g., "Read 150 lines")
|
|
57
|
+
def format_result(result)
|
|
58
|
+
if result.is_a?(Hash) && result[:message]
|
|
59
|
+
result[:message]
|
|
60
|
+
elsif result.is_a?(String)
|
|
61
|
+
result.length > 100 ? "#{result[0..100]}..." : result
|
|
62
|
+
else
|
|
63
|
+
"Done"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Format tool result as a structured hash for rich UI rendering.
|
|
68
|
+
# When a tool implements this, the WebUI can render a beautiful
|
|
69
|
+
# card instead of a plain text blob.
|
|
70
|
+
#
|
|
71
|
+
# @param result [Object] The result returned by execute
|
|
72
|
+
# @return [Hash, nil] A hash with :type and tool-specific fields,
|
|
73
|
+
# or nil to fall back to plain-text format_result.
|
|
74
|
+
#
|
|
75
|
+
# Supported types and their schemas:
|
|
76
|
+
#
|
|
77
|
+
# { type: "file_read", path:, lines_read:, total_lines:,
|
|
78
|
+
# truncated:, content_preview:, language: }
|
|
79
|
+
#
|
|
80
|
+
# { type: "file_list", path:, entries:[{name, is_dir}], total }
|
|
81
|
+
#
|
|
82
|
+
# { type: "search", pattern:, path:, matches:[{file, line_no, line, context?}],
|
|
83
|
+
# total_matches, files_with_matches, truncated }
|
|
84
|
+
#
|
|
85
|
+
# { type: "terminal", command:, exit_code:, output_preview:,
|
|
86
|
+
# output_truncated:, full_output_file? }
|
|
87
|
+
#
|
|
88
|
+
# { type: "web_fetch", url:, title?, content_preview: }
|
|
89
|
+
#
|
|
90
|
+
# { type: "web_search", query:, results:[{title, url, snippet}] }
|
|
91
|
+
#
|
|
92
|
+
# { type: "edit", path:, operation:, occurrences: }
|
|
93
|
+
#
|
|
94
|
+
# { type: "write", path:, is_new_file:, size_bytes: }
|
|
95
|
+
#
|
|
96
|
+
# { type: "todo", action:, todos:[{id, task, status}] }
|
|
97
|
+
#
|
|
98
|
+
# { type: "browser", action:, url?, title?, content_preview? }
|
|
99
|
+
#
|
|
100
|
+
# { type: "generic", title:, content:, status: "ok|error|warning" }
|
|
101
|
+
def format_result_for_ui(result)
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Convert to OpenAI function calling format
|
|
106
|
+
def to_function_definition
|
|
107
|
+
{
|
|
108
|
+
type: "function",
|
|
109
|
+
function: {
|
|
110
|
+
name: name,
|
|
111
|
+
description: description,
|
|
112
|
+
parameters: parameters
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|