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,260 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "cgi"
|
|
7
|
+
require "base64"
|
|
8
|
+
require_relative "../utils/encoding"
|
|
9
|
+
|
|
10
|
+
module Octo
|
|
11
|
+
module Tools
|
|
12
|
+
class WebSearch < Base
|
|
13
|
+
self.tool_name = "web_search"
|
|
14
|
+
self.tool_description = "Search the web for current information. Returns search results with titles, URLs, and snippets."
|
|
15
|
+
self.tool_category = "web"
|
|
16
|
+
self.tool_parameters = {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
query: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "The search query"
|
|
22
|
+
},
|
|
23
|
+
max_results: {
|
|
24
|
+
type: "integer",
|
|
25
|
+
description: "Maximum number of results to return (default: 10)",
|
|
26
|
+
default: 10
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
required: %w[query]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Ordered list of search providers to try in sequence.
|
|
33
|
+
# cn.bing.com is accessible in mainland China without VPN.
|
|
34
|
+
PROVIDERS = %i[duckduckgo bing].freeze
|
|
35
|
+
|
|
36
|
+
def execute(query:, max_results: 10, working_dir: nil)
|
|
37
|
+
if query.nil? || query.strip.empty?
|
|
38
|
+
return { error: "Query cannot be empty" }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
last_error = nil
|
|
42
|
+
|
|
43
|
+
providers = active_providers
|
|
44
|
+
providers.each do |provider|
|
|
45
|
+
begin
|
|
46
|
+
results = send(:"search_#{provider}", query, max_results)
|
|
47
|
+
# Consider it a success only if we got real results
|
|
48
|
+
next if results.empty? || results.first[:url].include?("duckduckgo.com") && results.first[:title] == "Web search results"
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
query: query,
|
|
52
|
+
results: results,
|
|
53
|
+
count: results.length,
|
|
54
|
+
provider: provider.to_s,
|
|
55
|
+
error: nil
|
|
56
|
+
}
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
# DuckDuckGo failed — suppress it for 10 minutes
|
|
59
|
+
@ddg_unavailable_until = Time.now + 600 if provider == :duckduckgo
|
|
60
|
+
last_error = e
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# All providers failed
|
|
66
|
+
{
|
|
67
|
+
query: query,
|
|
68
|
+
results: [],
|
|
69
|
+
count: 0,
|
|
70
|
+
provider: nil,
|
|
71
|
+
error: "All search providers failed. Last error: #{last_error&.message}"
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Skip DuckDuckGo if it failed recently (within last 10 minutes)
|
|
76
|
+
private def active_providers
|
|
77
|
+
if @ddg_unavailable_until && Time.now < @ddg_unavailable_until
|
|
78
|
+
PROVIDERS.drop(1)
|
|
79
|
+
else
|
|
80
|
+
PROVIDERS
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# ── DuckDuckGo ─────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
private def search_duckduckgo(query, max_results)
|
|
87
|
+
encoded_query = CGI.escape(query)
|
|
88
|
+
url = URI("https://html.duckduckgo.com/html/?q=#{encoded_query}")
|
|
89
|
+
response = http_get(url)
|
|
90
|
+
return [] unless response.is_a?(Net::HTTPSuccess)
|
|
91
|
+
|
|
92
|
+
parse_duckduckgo_html(response.body, max_results)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private def parse_duckduckgo_html(html, max_results)
|
|
96
|
+
results = []
|
|
97
|
+
html = Octo::Utils::Encoding.to_utf8(html)
|
|
98
|
+
|
|
99
|
+
links = html.scan(%r{<a[^>]*class="result__a"[^>]*href="//duckduckgo\.com/l/\?uddg=([^"&]+)[^"]*"[^>]*>(.*?)</a>}m)
|
|
100
|
+
snippets = html.scan(%r{<a[^>]*class="result__snippet"[^>]*>(.*?)</a>}m)
|
|
101
|
+
|
|
102
|
+
links.each_with_index do |link_data, index|
|
|
103
|
+
break if results.length >= max_results
|
|
104
|
+
|
|
105
|
+
url = Octo::Utils::Encoding.to_utf8(CGI.unescape(link_data[0]))
|
|
106
|
+
title = link_data[1].gsub(/<[^>]+>/, "").strip
|
|
107
|
+
title = CGI.unescapeHTML(title) if title.include?("&")
|
|
108
|
+
|
|
109
|
+
snippet = ""
|
|
110
|
+
if snippets[index]
|
|
111
|
+
snippet = snippets[index][0].gsub(/<[^>]+>/, "").strip
|
|
112
|
+
snippet = CGI.unescapeHTML(snippet) if snippet.include?("&")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
results << { title: title, url: url, snippet: snippet }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
results
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# ── Bing ───────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
private def search_bing(query, max_results)
|
|
124
|
+
encoded_query = CGI.escape(query)
|
|
125
|
+
# cn.bing.com redirects to www.bing.com for non-China IPs (e.g. GitHub CI);
|
|
126
|
+
# follow_redirects ensures both environments work with the same code path.
|
|
127
|
+
url = URI("https://cn.bing.com/search?q=#{encoded_query}&count=#{max_results}")
|
|
128
|
+
response = http_get(url, accept_language: "zh-CN,zh;q=0.9,en;q=0.8", follow_redirects: 2)
|
|
129
|
+
return [] unless response.is_a?(Net::HTTPSuccess)
|
|
130
|
+
|
|
131
|
+
parse_bing_html(response.body, max_results)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private def parse_bing_html(html, max_results)
|
|
135
|
+
results = []
|
|
136
|
+
html = Octo::Utils::Encoding.to_utf8(html)
|
|
137
|
+
|
|
138
|
+
# Bing result blocks: <li class="b_algo">...</li>
|
|
139
|
+
blocks = html.scan(%r{<li[^>]*class="b_algo"[^>]*>(.*?)</li>}m)
|
|
140
|
+
|
|
141
|
+
blocks.each do |block_arr|
|
|
142
|
+
break if results.length >= max_results
|
|
143
|
+
block = block_arr[0]
|
|
144
|
+
|
|
145
|
+
# Extract URL and title from <h2><a href="URL">TITLE</a></h2>
|
|
146
|
+
title_match = block.match(%r{<h2[^>]*>.*?<a[^>]*href="(https?://[^"]+)"[^>]*>(.*?)</a>}m)
|
|
147
|
+
next unless title_match
|
|
148
|
+
|
|
149
|
+
raw_url = CGI.unescapeHTML(title_match[1])
|
|
150
|
+
url = decode_bing_url(raw_url)
|
|
151
|
+
title = title_match[2].gsub(/<[^>]+>/, "").strip
|
|
152
|
+
title = CGI.unescapeHTML(title) if title.include?("&")
|
|
153
|
+
|
|
154
|
+
# Extract snippet from <p class="b_lineclamp..."> or <div class="b_caption"><p>
|
|
155
|
+
snippet = ""
|
|
156
|
+
snippet_match = block.match(%r{<p[^>]*class="b_lineclamp[^"]*"[^>]*>(.*?)</p>}m) ||
|
|
157
|
+
block.match(%r{<div[^>]*class="b_caption"[^>]*>.*?<p[^>]*>(.*?)</p>}m)
|
|
158
|
+
if snippet_match
|
|
159
|
+
snippet = snippet_match[1].gsub(/<[^>]+>/, "").strip
|
|
160
|
+
snippet = CGI.unescapeHTML(snippet) if snippet.include?("&")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
results << { title: title, url: url, snippet: snippet }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
results
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Decode Bing's redirect URL: bing.com/ck/a?...&u=a1BASE64URL&ntb=1
|
|
170
|
+
# The "u" param is "a1" prefix + base64-encoded real URL
|
|
171
|
+
private def decode_bing_url(url)
|
|
172
|
+
return url unless url.include?("bing.com/ck/")
|
|
173
|
+
|
|
174
|
+
u_param = url.match(/[?&]u=([^&]+)/)
|
|
175
|
+
return url unless u_param
|
|
176
|
+
|
|
177
|
+
encoded = u_param[1]
|
|
178
|
+
# Remove "a1" prefix then base64-decode
|
|
179
|
+
return url unless encoded.start_with?("a1")
|
|
180
|
+
|
|
181
|
+
base64_part = encoded[2..]
|
|
182
|
+
# Bing uses URL-safe base64 without padding
|
|
183
|
+
padded = base64_part + "=" * ((4 - base64_part.length % 4) % 4)
|
|
184
|
+
decoded = Base64.urlsafe_decode64(padded)
|
|
185
|
+
decoded.force_encoding("UTF-8").valid_encoding? ? decoded : url
|
|
186
|
+
rescue StandardError
|
|
187
|
+
url
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# ── Shared HTTP helper ─────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
USER_AGENTS = [
|
|
193
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
194
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
|
195
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
196
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
|
197
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
198
|
+
].freeze
|
|
199
|
+
|
|
200
|
+
# Shared browser-like GET request — no Accept-Encoding to avoid gzip/br
|
|
201
|
+
# detection tricks used by Bing. Supports redirect following.
|
|
202
|
+
private def http_get(url, accept_language: "en-US,en;q=0.9", follow_redirects: 0)
|
|
203
|
+
request = Net::HTTP::Get.new(url)
|
|
204
|
+
request["User-Agent"] = USER_AGENTS.sample
|
|
205
|
+
request["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
206
|
+
request["Accept-Language"] = accept_language
|
|
207
|
+
# Deliberately omit Accept-Encoding — sending gzip causes Bing to return
|
|
208
|
+
# a JS-only skeleton (~39KB) instead of the real HTML results (~120KB)
|
|
209
|
+
request["Sec-Fetch-Dest"] = "document"
|
|
210
|
+
request["Sec-Fetch-Mode"] = "navigate"
|
|
211
|
+
request["Sec-Fetch-Site"] = "none"
|
|
212
|
+
request["Upgrade-Insecure-Requests"] = "1"
|
|
213
|
+
|
|
214
|
+
response = Net::HTTP.start(url.hostname, url.port,
|
|
215
|
+
use_ssl: url.scheme == "https",
|
|
216
|
+
read_timeout: 8,
|
|
217
|
+
open_timeout: 5) { |http| http.request(request) }
|
|
218
|
+
|
|
219
|
+
# Follow redirects (e.g. cn.bing.com redirects to www.bing.com for non-China IPs)
|
|
220
|
+
if follow_redirects > 0 && response.is_a?(Net::HTTPRedirection)
|
|
221
|
+
location = response["location"]
|
|
222
|
+
redirect_url = location.start_with?("http") ? URI(location) : URI("#{url.scheme}://#{url.hostname}#{location}")
|
|
223
|
+
return http_get(redirect_url, accept_language: accept_language, follow_redirects: follow_redirects - 1)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
response
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# ── Formatting ─────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
def format_call(args)
|
|
232
|
+
query = args[:query] || args["query"] || ""
|
|
233
|
+
display_query = query.length > 40 ? "#{query[0..37]}..." : query
|
|
234
|
+
"web_search(\"#{display_query}\")"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def format_result(result)
|
|
238
|
+
if result[:error]
|
|
239
|
+
"[Error] #{result[:error]}"
|
|
240
|
+
else
|
|
241
|
+
count = result[:count] || 0
|
|
242
|
+
provider = result[:provider] ? " via #{result[:provider]}" : ""
|
|
243
|
+
"[OK] Found #{count} results#{provider}"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def format_result_for_ui(result)
|
|
248
|
+
return nil if result[:error]
|
|
249
|
+
{
|
|
250
|
+
type: "web_search",
|
|
251
|
+
query: result[:query],
|
|
252
|
+
results: (result[:results] || []).first(5).map do |r|
|
|
253
|
+
{ title: r[:title], url: r[:url], snippet: r[:snippet] }
|
|
254
|
+
end,
|
|
255
|
+
total: result[:count] || 0
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
module Tools
|
|
5
|
+
class Write < Base
|
|
6
|
+
self.tool_name = "write"
|
|
7
|
+
self.tool_description = "Write content to a file. Creates new files or overwrites existing ones."
|
|
8
|
+
self.tool_category = "file_system"
|
|
9
|
+
self.tool_parameters = {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
path: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "The path of the file to write (absolute or relative)"
|
|
15
|
+
},
|
|
16
|
+
content: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "The content to write to the file"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
required: %w[path content]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def execute(path:, content:, working_dir: nil)
|
|
25
|
+
# Validate path
|
|
26
|
+
if path.nil? || path.strip.empty?
|
|
27
|
+
return { error: "Path cannot be empty" }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
# Expand ~ to home directory, resolve relative paths against working_dir
|
|
32
|
+
path = expand_path(path, working_dir: working_dir)
|
|
33
|
+
|
|
34
|
+
# Ensure parent directory exists
|
|
35
|
+
dir = File.dirname(path)
|
|
36
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
37
|
+
|
|
38
|
+
# Write content to file
|
|
39
|
+
File.write(path, content)
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
path: File.expand_path(path),
|
|
43
|
+
bytes_written: content.bytesize,
|
|
44
|
+
error: nil
|
|
45
|
+
}
|
|
46
|
+
rescue Errno::EACCES => e
|
|
47
|
+
{ error: "Permission denied: #{e.message}" }
|
|
48
|
+
rescue Errno::ENOSPC => e
|
|
49
|
+
{ error: "No space left on device: #{e.message}" }
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
{ error: "Failed to write file: #{e.message}" }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def format_call(args)
|
|
56
|
+
path = args[:path] || args['path']
|
|
57
|
+
"Write(#{Utils::PathHelper.safe_basename(path)})"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def format_result(result)
|
|
61
|
+
return result[:error] if result[:error]
|
|
62
|
+
|
|
63
|
+
bytes = result[:bytes_written] || result['bytes_written'] || 0
|
|
64
|
+
"Written #{bytes} bytes"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def format_result_for_ui(result)
|
|
68
|
+
return nil if result[:error]
|
|
69
|
+
{
|
|
70
|
+
type: "write",
|
|
71
|
+
path: result[:path],
|
|
72
|
+
size_bytes: result[:bytes_written] || 0
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module Octo
|
|
6
|
+
module UI2
|
|
7
|
+
module Components
|
|
8
|
+
# BaseComponent provides common functionality for all UI components
|
|
9
|
+
class BaseComponent
|
|
10
|
+
def initialize
|
|
11
|
+
@pastel = Pastel.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Render component with given data
|
|
15
|
+
# @param data [Hash] Data to render
|
|
16
|
+
# @return [String] Rendered output
|
|
17
|
+
def render(data)
|
|
18
|
+
raise NotImplementedError, "Subclasses must implement render method"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Class method to render without instantiating
|
|
22
|
+
# @param data [Hash] Data to render
|
|
23
|
+
# @return [String] Rendered output
|
|
24
|
+
def self.render(data)
|
|
25
|
+
new.render(data)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
protected
|
|
29
|
+
|
|
30
|
+
# Get current theme from ThemeManager
|
|
31
|
+
# @return [Themes::BaseTheme] Current theme instance
|
|
32
|
+
def theme
|
|
33
|
+
UI2::ThemeManager.current_theme
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Format symbol with color from theme
|
|
37
|
+
# @param symbol_key [Symbol] Symbol key (e.g., :user, :assistant)
|
|
38
|
+
# @return [String] Colored symbol
|
|
39
|
+
def format_symbol(symbol_key)
|
|
40
|
+
theme.format_symbol(symbol_key)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Format text with color from theme
|
|
44
|
+
# @param text [String] Text to format
|
|
45
|
+
# @param symbol_key [Symbol] Symbol key for color lookup
|
|
46
|
+
# @return [String] Colored text
|
|
47
|
+
def format_text(text, symbol_key)
|
|
48
|
+
theme.format_text(text, symbol_key)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Truncate text to max length
|
|
52
|
+
# @param text [String] Text to truncate
|
|
53
|
+
# @param max_length [Integer] Maximum length
|
|
54
|
+
# @return [String] Truncated text
|
|
55
|
+
def truncate(text, max_length)
|
|
56
|
+
return "" if text.nil? || text.empty?
|
|
57
|
+
|
|
58
|
+
cleaned = text.strip.gsub(/\s+/, ' ')
|
|
59
|
+
|
|
60
|
+
if cleaned.length > max_length
|
|
61
|
+
cleaned[0...max_length] + "..."
|
|
62
|
+
else
|
|
63
|
+
cleaned
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Wrap text to specified width
|
|
68
|
+
# @param text [String] Text to wrap
|
|
69
|
+
# @param width [Integer] Maximum width
|
|
70
|
+
# @return [Array<String>] Array of wrapped lines
|
|
71
|
+
def wrap_text(text, width)
|
|
72
|
+
return [] if text.nil? || text.empty?
|
|
73
|
+
|
|
74
|
+
words = text.split(/\s+/)
|
|
75
|
+
lines = []
|
|
76
|
+
current_line = ""
|
|
77
|
+
|
|
78
|
+
words.each do |word|
|
|
79
|
+
if current_line.empty?
|
|
80
|
+
current_line = word
|
|
81
|
+
elsif (current_line.length + word.length + 1) <= width
|
|
82
|
+
current_line += " #{word}"
|
|
83
|
+
else
|
|
84
|
+
lines << current_line
|
|
85
|
+
current_line = word
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
lines << current_line unless current_line.empty?
|
|
90
|
+
lines
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Format timestamp
|
|
94
|
+
# @param time [Time] Time object
|
|
95
|
+
# @return [String] Formatted timestamp
|
|
96
|
+
def format_timestamp(time = Time.now)
|
|
97
|
+
time.strftime("%H:%M:%S")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Create indented text
|
|
101
|
+
# @param text [String] Text to indent
|
|
102
|
+
# @param spaces [Integer] Number of spaces
|
|
103
|
+
# @return [String] Indented text
|
|
104
|
+
def indent(text, spaces = 2)
|
|
105
|
+
prefix = " " * spaces
|
|
106
|
+
text.split("\n").map { |line| "#{prefix}#{line}" }.join("\n")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Format key-value pair
|
|
110
|
+
# @param key [String] Key name
|
|
111
|
+
# @param value [String] Value
|
|
112
|
+
# @return [String] Formatted key-value
|
|
113
|
+
def format_key_value(key, value)
|
|
114
|
+
"#{@pastel.cyan(key)}: #{@pastel.white(value)}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Create a separator line
|
|
118
|
+
# @param char [String] Character to use
|
|
119
|
+
# @param width [Integer] Width of separator
|
|
120
|
+
# @return [String] Separator line
|
|
121
|
+
def separator(char = "─", width = 80)
|
|
122
|
+
@pastel.dim(char * width)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Format list item
|
|
126
|
+
# @param text [String] Item text
|
|
127
|
+
# @param bullet [String] Bullet character
|
|
128
|
+
# @return [String] Formatted list item
|
|
129
|
+
def format_list_item(text, bullet = "•")
|
|
130
|
+
"#{@pastel.dim(bullet)} #{@pastel.white(text)}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Format code block
|
|
134
|
+
# @param code [String] Code content
|
|
135
|
+
# @param language [String, nil] Language for syntax highlighting hint
|
|
136
|
+
# @return [String] Formatted code block
|
|
137
|
+
def format_code_block(code, language = nil)
|
|
138
|
+
header = language ? @pastel.dim("```#{language}") : @pastel.dim("```")
|
|
139
|
+
footer = @pastel.dim("```")
|
|
140
|
+
content = @pastel.cyan(code)
|
|
141
|
+
|
|
142
|
+
"#{header}\n#{content}\n#{footer}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Format progress bar
|
|
146
|
+
# @param current [Integer] Current value
|
|
147
|
+
# @param total [Integer] Total value
|
|
148
|
+
# @param width [Integer] Bar width
|
|
149
|
+
# @return [String] Progress bar
|
|
150
|
+
def format_progress_bar(current, total, width = 20)
|
|
151
|
+
return "" if total == 0
|
|
152
|
+
|
|
153
|
+
percentage = (current.to_f / total * 100).round(1)
|
|
154
|
+
filled = (current.to_f / total * width).round
|
|
155
|
+
empty = width - filled
|
|
156
|
+
|
|
157
|
+
bar = @pastel.green("█" * filled) + @pastel.dim("░" * empty)
|
|
158
|
+
"#{bar} #{percentage}%"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|