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.
Files changed (319) hide show
  1. checksums.yaml +7 -0
  2. data/.clacky/skills/commit/SKILL.md +423 -0
  3. data/.clacky/skills/gem-release/SKILL.md +199 -0
  4. data/.clacky/skills/gem-release/scripts/release.sh +304 -0
  5. data/.clacky/skills/oss-upload/SKILL.md +47 -0
  6. data/.octorules +106 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +8 -0
  9. data/CHANGELOG.md +76 -0
  10. data/CODE_OF_CONDUCT.md +132 -0
  11. data/CONTRIBUTING.md +92 -0
  12. data/Dockerfile +28 -0
  13. data/LICENSE.txt +22 -0
  14. data/POSITIONING.md +46 -0
  15. data/README.md +134 -0
  16. data/README_CN.md +134 -0
  17. data/Rakefile +34 -0
  18. data/benchmark/fixtures/sample_project/Gemfile +3 -0
  19. data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
  20. data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
  21. data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
  22. data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
  23. data/benchmark/results/EVALUATION_REPORT.md +165 -0
  24. data/benchmark/results/baseline_20260511_174424.json +128 -0
  25. data/benchmark/results/report_20260511_175256.json +271 -0
  26. data/benchmark/results/report_20260511_175444.json +271 -0
  27. data/benchmark/results/treatment_20260511_175103.json +130 -0
  28. data/benchmark/runner.rb +441 -0
  29. data/bin/octo +7 -0
  30. data/docs/agent-first-ui-design.md +77 -0
  31. data/docs/billing-system.md +318 -0
  32. data/docs/channel-architecture.md +235 -0
  33. data/docs/engineering-article.md +343 -0
  34. data/docs/session-skill-invocation.md +69 -0
  35. data/docs/time_machine_design.md +247 -0
  36. data/docs/ui2-architecture.md +124 -0
  37. data/homebrew/README.md +96 -0
  38. data/homebrew/openocto.rb +24 -0
  39. data/lib/octo/agent/hook_manager.rb +61 -0
  40. data/lib/octo/agent/llm_caller.rb +800 -0
  41. data/lib/octo/agent/memory_updater.rb +246 -0
  42. data/lib/octo/agent/message_compressor.rb +225 -0
  43. data/lib/octo/agent/message_compressor_helper.rb +869 -0
  44. data/lib/octo/agent/next_message_suggester.rb +215 -0
  45. data/lib/octo/agent/session_serializer.rb +685 -0
  46. data/lib/octo/agent/skill_auto_creator.rb +114 -0
  47. data/lib/octo/agent/skill_evolution.rb +61 -0
  48. data/lib/octo/agent/skill_manager.rb +466 -0
  49. data/lib/octo/agent/skill_reflector.rb +89 -0
  50. data/lib/octo/agent/system_prompt_builder.rb +101 -0
  51. data/lib/octo/agent/time_machine.rb +214 -0
  52. data/lib/octo/agent/tool_executor.rb +454 -0
  53. data/lib/octo/agent/tool_registry.rb +150 -0
  54. data/lib/octo/agent.rb +2180 -0
  55. data/lib/octo/agent_config.rb +989 -0
  56. data/lib/octo/agent_profile.rb +112 -0
  57. data/lib/octo/anthropic_stream_aggregator.rb +137 -0
  58. data/lib/octo/background_task_registry.rb +324 -0
  59. data/lib/octo/banner.rb +34 -0
  60. data/lib/octo/bedrock_stream_aggregator.rb +137 -0
  61. data/lib/octo/block_font.rb +331 -0
  62. data/lib/octo/cli.rb +968 -0
  63. data/lib/octo/client.rb +623 -0
  64. data/lib/octo/default_agents/SOUL.md +3 -0
  65. data/lib/octo/default_agents/USER.md +1 -0
  66. data/lib/octo/default_agents/base_prompt.md +66 -0
  67. data/lib/octo/default_agents/coding/profile.yml +2 -0
  68. data/lib/octo/default_agents/coding/system_prompt.md +67 -0
  69. data/lib/octo/default_agents/general/profile.yml +2 -0
  70. data/lib/octo/default_agents/general/system_prompt.md +16 -0
  71. data/lib/octo/default_parsers/doc_parser.rb +69 -0
  72. data/lib/octo/default_parsers/docx_parser.rb +188 -0
  73. data/lib/octo/default_parsers/pdf_parser.rb +120 -0
  74. data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
  75. data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
  76. data/lib/octo/default_parsers/pptx_parser.rb +140 -0
  77. data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
  78. data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
  79. data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
  80. data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
  81. data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
  82. data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
  83. data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
  84. data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
  85. data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
  86. data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
  87. data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
  88. data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
  89. data/lib/octo/default_skills/onboard/SKILL.md +578 -0
  90. data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
  91. data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
  92. data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
  93. data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
  94. data/lib/octo/default_skills/personal-website/publish.rb +235 -0
  95. data/lib/octo/default_skills/product-help/SKILL.md +123 -0
  96. data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
  97. data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
  98. data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
  99. data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
  100. data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
  101. data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
  102. data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
  103. data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
  104. data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
  105. data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
  106. data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
  107. data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
  108. data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
  109. data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
  110. data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
  111. data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
  112. data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
  113. data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
  114. data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
  115. data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
  116. data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
  117. data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
  118. data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
  119. data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
  120. data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
  121. data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  122. data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  123. data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
  124. data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
  125. data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  126. data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
  127. data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
  128. data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
  129. data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
  130. data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
  131. data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
  132. data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
  133. data/lib/octo/idle_compression_timer.rb +115 -0
  134. data/lib/octo/json_ui_controller.rb +204 -0
  135. data/lib/octo/message_format/anthropic.rb +409 -0
  136. data/lib/octo/message_format/bedrock.rb +361 -0
  137. data/lib/octo/message_format/open_ai.rb +222 -0
  138. data/lib/octo/message_history.rb +373 -0
  139. data/lib/octo/openai_stream_aggregator.rb +130 -0
  140. data/lib/octo/plain_ui_controller.rb +166 -0
  141. data/lib/octo/providers.rb +534 -0
  142. data/lib/octo/server/browser_manager.rb +397 -0
  143. data/lib/octo/server/channel/adapters/base.rb +82 -0
  144. data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
  145. data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
  146. data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
  147. data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
  148. data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
  149. data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
  150. data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
  151. data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
  152. data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
  153. data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
  154. data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
  155. data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
  156. data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
  157. data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
  158. data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
  159. data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
  160. data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
  161. data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
  162. data/lib/octo/server/channel/channel_config.rb +178 -0
  163. data/lib/octo/server/channel/channel_manager.rb +468 -0
  164. data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
  165. data/lib/octo/server/channel.rb +33 -0
  166. data/lib/octo/server/discover.rb +77 -0
  167. data/lib/octo/server/epipe_safe_io.rb +105 -0
  168. data/lib/octo/server/http_server.rb +3554 -0
  169. data/lib/octo/server/scheduler.rb +317 -0
  170. data/lib/octo/server/server_master.rb +325 -0
  171. data/lib/octo/server/session_registry.rb +431 -0
  172. data/lib/octo/server/web_ui_controller.rb +487 -0
  173. data/lib/octo/session_manager.rb +385 -0
  174. data/lib/octo/skill.rb +466 -0
  175. data/lib/octo/skill_loader.rb +328 -0
  176. data/lib/octo/tools/base.rb +118 -0
  177. data/lib/octo/tools/browser.rb +625 -0
  178. data/lib/octo/tools/edit.rb +165 -0
  179. data/lib/octo/tools/file_reader.rb +549 -0
  180. data/lib/octo/tools/glob.rb +162 -0
  181. data/lib/octo/tools/grep.rb +356 -0
  182. data/lib/octo/tools/invoke_skill.rb +96 -0
  183. data/lib/octo/tools/list_tasks.rb +54 -0
  184. data/lib/octo/tools/redo_task.rb +41 -0
  185. data/lib/octo/tools/request_user_feedback.rb +84 -0
  186. data/lib/octo/tools/security.rb +333 -0
  187. data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
  188. data/lib/octo/tools/terminal/persistent_session.rb +268 -0
  189. data/lib/octo/tools/terminal/safe_rm.sh +106 -0
  190. data/lib/octo/tools/terminal/session_manager.rb +213 -0
  191. data/lib/octo/tools/terminal.rb +1828 -0
  192. data/lib/octo/tools/todo_manager.rb +374 -0
  193. data/lib/octo/tools/trash_manager.rb +388 -0
  194. data/lib/octo/tools/undo_task.rb +35 -0
  195. data/lib/octo/tools/web_fetch.rb +242 -0
  196. data/lib/octo/tools/web_search.rb +260 -0
  197. data/lib/octo/tools/write.rb +77 -0
  198. data/lib/octo/ui2/block_font.rb +10 -0
  199. data/lib/octo/ui2/components/base_component.rb +163 -0
  200. data/lib/octo/ui2/components/command_suggestions.rb +290 -0
  201. data/lib/octo/ui2/components/common_component.rb +96 -0
  202. data/lib/octo/ui2/components/inline_input.rb +226 -0
  203. data/lib/octo/ui2/components/input_area.rb +1338 -0
  204. data/lib/octo/ui2/components/message_component.rb +99 -0
  205. data/lib/octo/ui2/components/modal_component.rb +419 -0
  206. data/lib/octo/ui2/components/todo_area.rb +149 -0
  207. data/lib/octo/ui2/components/tool_component.rb +107 -0
  208. data/lib/octo/ui2/components/welcome_banner.rb +139 -0
  209. data/lib/octo/ui2/layout_manager.rb +807 -0
  210. data/lib/octo/ui2/line_editor.rb +363 -0
  211. data/lib/octo/ui2/markdown_renderer.rb +100 -0
  212. data/lib/octo/ui2/output_buffer.rb +370 -0
  213. data/lib/octo/ui2/progress_handle.rb +362 -0
  214. data/lib/octo/ui2/progress_indicator.rb +55 -0
  215. data/lib/octo/ui2/screen_buffer.rb +273 -0
  216. data/lib/octo/ui2/terminal_detector.rb +119 -0
  217. data/lib/octo/ui2/theme_manager.rb +85 -0
  218. data/lib/octo/ui2/themes/base_theme.rb +105 -0
  219. data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
  220. data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
  221. data/lib/octo/ui2/thinking_verbs.rb +26 -0
  222. data/lib/octo/ui2/ui_controller.rb +1625 -0
  223. data/lib/octo/ui2/view_renderer.rb +177 -0
  224. data/lib/octo/ui2.rb +40 -0
  225. data/lib/octo/ui_interface.rb +154 -0
  226. data/lib/octo/utils/arguments_parser.rb +191 -0
  227. data/lib/octo/utils/browser_detector.rb +195 -0
  228. data/lib/octo/utils/encoding.rb +92 -0
  229. data/lib/octo/utils/environment_detector.rb +140 -0
  230. data/lib/octo/utils/file_ignore_helper.rb +170 -0
  231. data/lib/octo/utils/file_processor.rb +601 -0
  232. data/lib/octo/utils/gitignore_parser.rb +154 -0
  233. data/lib/octo/utils/limit_stack.rb +152 -0
  234. data/lib/octo/utils/logger.rb +124 -0
  235. data/lib/octo/utils/login_shell.rb +72 -0
  236. data/lib/octo/utils/model_pricing.rb +646 -0
  237. data/lib/octo/utils/parser_manager.rb +165 -0
  238. data/lib/octo/utils/path_helper.rb +15 -0
  239. data/lib/octo/utils/scripts_manager.rb +59 -0
  240. data/lib/octo/utils/string_matcher.rb +158 -0
  241. data/lib/octo/utils/trash_directory.rb +112 -0
  242. data/lib/octo/utils/workspace_rules.rb +46 -0
  243. data/lib/octo/version.rb +5 -0
  244. data/lib/octo/web/app.css +7141 -0
  245. data/lib/octo/web/app.js +543 -0
  246. data/lib/octo/web/apple-touch-icon.png +0 -0
  247. data/lib/octo/web/auth.js +150 -0
  248. data/lib/octo/web/channels.js +276 -0
  249. data/lib/octo/web/datepicker.js +205 -0
  250. data/lib/octo/web/favicon.png +0 -0
  251. data/lib/octo/web/i18n.js +1073 -0
  252. data/lib/octo/web/icon-512.png +0 -0
  253. data/lib/octo/web/icon-dark.svg +25 -0
  254. data/lib/octo/web/icon.svg +29 -0
  255. data/lib/octo/web/index.html +871 -0
  256. data/lib/octo/web/marked.min.js +69 -0
  257. data/lib/octo/web/onboard.js +491 -0
  258. data/lib/octo/web/profile.js +442 -0
  259. data/lib/octo/web/sessions.js +4421 -0
  260. data/lib/octo/web/settings.js +913 -0
  261. data/lib/octo/web/sidebar.js +32 -0
  262. data/lib/octo/web/skills.js +885 -0
  263. data/lib/octo/web/tasks.js +297 -0
  264. data/lib/octo/web/theme.js +105 -0
  265. data/lib/octo/web/trash.js +343 -0
  266. data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
  267. data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
  268. data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
  269. data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  270. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  271. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  272. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  273. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  274. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  275. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  276. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  277. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  278. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  279. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  280. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  281. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  282. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  283. data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  284. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  285. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  286. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  287. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  288. data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  289. data/lib/octo/web/vendor/katex/katex.min.css +1 -0
  290. data/lib/octo/web/vendor/katex/katex.min.js +1 -0
  291. data/lib/octo/web/version.js +449 -0
  292. data/lib/octo/web/weixin-qr.html +209 -0
  293. data/lib/octo/web/ws-dispatcher.js +357 -0
  294. data/lib/octo/web/ws.js +128 -0
  295. data/lib/octo.rb +145 -0
  296. data/scripts/build/build.sh +329 -0
  297. data/scripts/build/lib/apt.sh +56 -0
  298. data/scripts/build/lib/brew.sh +89 -0
  299. data/scripts/build/lib/colors.sh +17 -0
  300. data/scripts/build/lib/gem.sh +95 -0
  301. data/scripts/build/lib/mise.sh +125 -0
  302. data/scripts/build/lib/network.sh +157 -0
  303. data/scripts/build/lib/os.sh +57 -0
  304. data/scripts/build/lib/shell.sh +37 -0
  305. data/scripts/build/src/install.sh.cc +174 -0
  306. data/scripts/build/src/install_browser.sh.cc +101 -0
  307. data/scripts/build/src/install_full.sh.cc +290 -0
  308. data/scripts/build/src/install_rails_deps.sh.cc +145 -0
  309. data/scripts/build/src/install_system_deps.sh.cc +123 -0
  310. data/scripts/build/src/uninstall.sh.cc +101 -0
  311. data/scripts/install.ps1 +532 -0
  312. data/scripts/install.sh +567 -0
  313. data/scripts/install_browser.sh +479 -0
  314. data/scripts/install_full.sh +838 -0
  315. data/scripts/install_rails_deps.sh +746 -0
  316. data/scripts/install_system_deps.sh +518 -0
  317. data/scripts/uninstall.sh +287 -0
  318. data/sig/octo.rbs +4 -0
  319. 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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../block_font"
4
+
5
+ module Octo
6
+ module UI2
7
+ # Alias for backward compatibility — BlockFont now lives at Octo::BlockFont.
8
+ BlockFont = Octo::BlockFont
9
+ end
10
+ 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