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,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Octo
6
+ module Tools
7
+ class Glob < Base
8
+ # Maximum file size to search (1MB)
9
+ MAX_FILE_SIZE = 1_048_576
10
+
11
+ self.tool_name = "glob"
12
+ self.tool_description = "Find files matching a glob pattern (e.g., '**/*.rb', 'src/**/*.js'). " \
13
+ "Returns file paths sorted by modification time. Respects .gitignore patterns."
14
+ self.tool_category = "file_system"
15
+ self.tool_parameters = {
16
+ type: "object",
17
+ properties: {
18
+ pattern: {
19
+ type: "string",
20
+ description: "The glob pattern to match files (e.g., '**/*.rb', 'lib/**/*.rb', '*.txt')"
21
+ },
22
+ base_path: {
23
+ type: "string",
24
+ description: "The base directory to search in (defaults to current directory)",
25
+ default: "."
26
+ },
27
+ limit: {
28
+ type: "integer",
29
+ description: "Maximum number of results to return (default: 10)",
30
+ default: 10
31
+ }
32
+ },
33
+ required: %w[pattern]
34
+ }
35
+
36
+ def execute(pattern:, base_path: ".", limit: 10, working_dir: nil)
37
+ # Validate pattern
38
+ if pattern.nil? || pattern.strip.empty?
39
+ return { error: "Pattern cannot be empty" }
40
+ end
41
+
42
+ # Expand ~ in pattern only (pattern is relative to base_path, not working_dir)
43
+ pattern = pattern.start_with?("~") ? File.expand_path(pattern) : pattern
44
+ # Expand base_path fully (~ and relative paths resolved against working_dir)
45
+ base_path = expand_path(base_path, working_dir: working_dir)
46
+
47
+ # Validate base_path
48
+ unless Dir.exist?(base_path)
49
+ return { error: "Base path does not exist: #{base_path}" }
50
+ end
51
+
52
+ begin
53
+ expanded_path = base_path
54
+
55
+ skipped = {
56
+ binary: 0,
57
+ too_large: 0,
58
+ ignored: 0
59
+ }
60
+
61
+ # Auto-expand bare patterns (no slash, no **) to recursive search.
62
+ effective_pattern = if !File.absolute_path?(pattern) &&
63
+ !pattern.include?("/") &&
64
+ !pattern.start_with?("**")
65
+ "**/#{pattern}"
66
+ else
67
+ pattern
68
+ end
69
+
70
+ fnmatch_flags = File::FNM_PATHNAME | File::FNM_DOTMATCH
71
+
72
+ matches = []
73
+ Octo::Utils::FileIgnoreHelper.walk_files(expanded_path, skipped: skipped) do |file|
74
+ relative = file[(expanded_path.length + 1)..]
75
+
76
+ unless File.fnmatch(effective_pattern, relative, fnmatch_flags)
77
+ next
78
+ end
79
+
80
+ if Octo::Utils::FileProcessor.binary_file_path?(file) &&
81
+ !Octo::Utils::FileProcessor.glob_allowed_binary?(file)
82
+ skipped[:binary] += 1
83
+ next
84
+ end
85
+
86
+ if File.size(file) > MAX_FILE_SIZE
87
+ skipped[:too_large] += 1
88
+ next
89
+ end
90
+
91
+ matches << file
92
+ end
93
+
94
+ # Sort by modification time (most recent first)
95
+ matches = matches.sort_by { |path| -File.mtime(path).to_i }
96
+
97
+ # Apply limit
98
+ total_matches = matches.length
99
+ matches = matches.take(limit)
100
+
101
+ # Convert to absolute paths
102
+ matches = matches.map { |path| File.expand_path(path) }
103
+
104
+ {
105
+ matches: matches,
106
+ total_matches: total_matches,
107
+ returned: matches.length,
108
+ truncated: total_matches > limit,
109
+ skipped_files: skipped,
110
+ error: nil
111
+ }
112
+ rescue StandardError => e
113
+ { error: "Failed to glob files: #{e.message}" }
114
+ end
115
+ end
116
+
117
+ def format_call(args)
118
+ pattern = args[:pattern] || args['pattern'] || ''
119
+ base_path = args[:base_path] || args['base_path'] || '.'
120
+
121
+ display_base = base_path == '.' ? '' : " in #{base_path}"
122
+ "glob(\"#{pattern}\"#{display_base})"
123
+ end
124
+
125
+ def format_result(result)
126
+ if result[:error]
127
+ "[Error] #{result[:error]}"
128
+ else
129
+ count = result[:returned] || 0
130
+ total = result[:total_matches] || 0
131
+ truncated = result[:truncated] ? " (truncated)" : ""
132
+
133
+ msg = "[OK] Found #{count}/#{total} files#{truncated}"
134
+
135
+ # Add skipped files info if present
136
+ if result[:skipped_files]
137
+ skipped = result[:skipped_files]
138
+ skipped_parts = []
139
+ skipped_parts << "#{skipped[:ignored]} ignored" if skipped[:ignored] > 0
140
+ skipped_parts << "#{skipped[:binary]} binary" if skipped[:binary] > 0
141
+ skipped_parts << "#{skipped[:too_large]} too large" if skipped[:too_large] > 0
142
+
143
+ msg += " (skipped: #{skipped_parts.join(', ')})" unless skipped_parts.empty?
144
+ end
145
+
146
+ msg
147
+ end
148
+ end
149
+
150
+ def format_result_for_ui(result)
151
+ return nil if result[:error]
152
+ {
153
+ type: "file_list",
154
+ path: result[:base_path] || ".",
155
+ entries: (result[:matches] || []).map { |p| { name: File.basename(p), is_dir: false } },
156
+ total: result[:total_matches] || 0,
157
+ truncated: result[:truncated] || false
158
+ }
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,356 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ module Tools
5
+ class Grep < Base
6
+ # Maximum file size to search (1MB)
7
+ MAX_FILE_SIZE = 1_048_576
8
+
9
+ # Maximum line length to display (to avoid huge outputs)
10
+ MAX_LINE_LENGTH = 500
11
+
12
+ self.tool_name = "grep"
13
+ self.tool_description = "Search file contents using regular expressions. Returns matching lines with context."
14
+ self.tool_category = "file_system"
15
+ self.tool_parameters = {
16
+ type: "object",
17
+ properties: {
18
+ pattern: {
19
+ type: "string",
20
+ description: "The regular expression pattern to search for"
21
+ },
22
+ path: {
23
+ type: "string",
24
+ description: "File or directory to search in (defaults to current directory)",
25
+ default: "."
26
+ },
27
+ file_pattern: {
28
+ type: "string",
29
+ description: "Glob pattern to filter files (e.g., '*.rb', '**/*.js')",
30
+ default: "**/*"
31
+ },
32
+ case_insensitive: {
33
+ type: "boolean",
34
+ description: "Perform case-insensitive search",
35
+ default: false
36
+ },
37
+ context_lines: {
38
+ type: "integer",
39
+ description: "Number of context lines to show before and after each match (max: 10)",
40
+ default: 0
41
+ },
42
+ max_files: {
43
+ type: "integer",
44
+ description: "Maximum number of matching files to return",
45
+ default: 50
46
+ },
47
+ max_matches_per_file: {
48
+ type: "integer",
49
+ description: "Maximum number of matches to return per file",
50
+ default: 50
51
+ },
52
+ max_total_matches: {
53
+ type: "integer",
54
+ description: "Maximum total number of matches to return across all files",
55
+ default: 200
56
+ },
57
+ max_file_size: {
58
+ type: "integer",
59
+ description: "Maximum file size in bytes to search (default: 1MB)",
60
+ default: MAX_FILE_SIZE
61
+ },
62
+ },
63
+ required: %w[pattern]
64
+ }
65
+
66
+ def execute(
67
+ pattern:,
68
+ path: ".",
69
+ file_pattern: "**/*",
70
+ case_insensitive: false,
71
+ context_lines: 0,
72
+ max_files: 50,
73
+ max_matches_per_file: 50,
74
+ max_total_matches: 200,
75
+ max_file_size: MAX_FILE_SIZE,
76
+ max_files_to_search: 10000,
77
+ working_dir: nil
78
+ )
79
+ # Validate pattern
80
+ if pattern.nil? || pattern.strip.empty?
81
+ return { error: "Pattern cannot be empty" }
82
+ end
83
+
84
+ # Validate and expand path relative to working_dir when provided
85
+ begin
86
+ expanded_path = expand_path(path, working_dir: working_dir)
87
+ rescue StandardError => e
88
+ return { error: "Invalid path: #{e.message}" }
89
+ end
90
+
91
+ unless File.exist?(expanded_path)
92
+ return { error: "Path does not exist: #{path}" }
93
+ end
94
+
95
+ # Limit context_lines
96
+ context_lines = [[context_lines, 0].max, 10].min
97
+
98
+ begin
99
+ # Compile regex
100
+ regex_options = case_insensitive ? Regexp::IGNORECASE : 0
101
+ regex = Regexp.new(pattern, regex_options)
102
+
103
+ results = []
104
+ total_matches = 0
105
+ files_searched = 0
106
+ skipped = {
107
+ binary: 0,
108
+ too_large: 0,
109
+ ignored: 0
110
+ }
111
+ truncation_reason = nil
112
+
113
+ files = if File.file?(expanded_path)
114
+ [expanded_path]
115
+ else
116
+ fnmatch_flags = File::FNM_PATHNAME | File::FNM_DOTMATCH
117
+ collected = []
118
+ Octo::Utils::FileIgnoreHelper.walk_files(expanded_path, skipped: skipped) do |f|
119
+ relative = f[(expanded_path.length + 1)..]
120
+ collected << f if File.fnmatch(file_pattern, relative, fnmatch_flags)
121
+ end
122
+ collected
123
+ end
124
+
125
+ files.each do |file|
126
+ if files_searched >= max_files_to_search
127
+ truncation_reason ||= "max_files_to_search limit reached"
128
+ break
129
+ end
130
+
131
+ # Skip binary files
132
+ if Octo::Utils::FileProcessor.binary_file_path?(file)
133
+ skipped[:binary] += 1
134
+ next
135
+ end
136
+
137
+ # Skip files that are too large
138
+ if File.size(file) > max_file_size
139
+ skipped[:too_large] += 1
140
+ next
141
+ end
142
+
143
+ files_searched += 1
144
+
145
+ # Check if we've found enough matching files
146
+ if results.length >= max_files
147
+ truncation_reason ||= "max_files limit reached"
148
+ break
149
+ end
150
+
151
+ # Check if we've found enough total matches
152
+ if total_matches >= max_total_matches
153
+ truncation_reason ||= "max_total_matches limit reached"
154
+ break
155
+ end
156
+
157
+ # Search the file
158
+ matches = search_file(file, regex, context_lines, max_matches_per_file)
159
+ next if matches.empty?
160
+
161
+ # Add remaining matches respecting max_total_matches
162
+ remaining_matches = max_total_matches - total_matches
163
+ matches = matches.take(remaining_matches) if remaining_matches < matches.length
164
+
165
+ results << {
166
+ file: File.expand_path(file),
167
+ matches: matches
168
+ }
169
+ total_matches += matches.length
170
+ end
171
+
172
+ {
173
+ results: results,
174
+ total_matches: total_matches,
175
+ files_searched: files_searched,
176
+ files_with_matches: results.length,
177
+ skipped_files: skipped,
178
+ truncated: !truncation_reason.nil?,
179
+ truncation_reason: truncation_reason,
180
+ error: nil
181
+ }
182
+ rescue RegexpError => e
183
+ { error: "Invalid regex pattern: #{e.message}" }
184
+ rescue StandardError => e
185
+ { error: "Failed to search files: #{e.message}" }
186
+ end
187
+ end
188
+
189
+ def format_call(args)
190
+ pattern = args[:pattern] || args['pattern'] || ''
191
+ path = args[:path] || args['path'] || '.'
192
+
193
+ # Truncate pattern if too long
194
+ display_pattern = pattern.length > 30 ? "#{pattern[0..27]}..." : pattern
195
+ display_path = path == '.' ? 'current dir' : (path.length > 20 ? "...#{path[-17..]}" : path)
196
+
197
+ "grep(\"#{display_pattern}\" in #{display_path})"
198
+ end
199
+
200
+ def format_result(result)
201
+ if result[:error]
202
+ "[Error] #{result[:error]}"
203
+ else
204
+ matches = result[:total_matches] || 0
205
+ files = result[:files_with_matches] || 0
206
+ msg = "[OK] Found #{matches} matches in #{files} files"
207
+
208
+ # Add truncation info if present
209
+ if result[:truncated] && result[:truncation_reason]
210
+ msg += " (truncated: #{result[:truncation_reason]})"
211
+ end
212
+
213
+ msg
214
+ end
215
+ end
216
+
217
+ def format_result_for_ui(result)
218
+ return nil if result[:error]
219
+
220
+ matches = result[:results]&.flat_map do |r|
221
+ r[:matches]&.map do |m|
222
+ {
223
+ file: r[:file],
224
+ line_no: m[:line_number],
225
+ line: m[:line],
226
+ context: m[:context]
227
+ }
228
+ end
229
+ end&.compact || []
230
+
231
+ {
232
+ type: "search",
233
+ pattern: result[:pattern],
234
+ path: result[:path],
235
+ matches: matches.first(20),
236
+ total_matches: result[:total_matches],
237
+ files_with_matches: result[:files_with_matches],
238
+ truncated: result[:truncated],
239
+ truncation_reason: result[:truncation_reason]
240
+ }
241
+ end
242
+
243
+ # Format result for LLM consumption - return a compact version to save tokens
244
+ def format_result_for_llm(result)
245
+ # If there's an error, return it as-is
246
+ return result if result[:error]
247
+
248
+ # Build a compact summary with file list and sample matches
249
+ compact = {
250
+ summary: {
251
+ total_matches: result[:total_matches],
252
+ files_with_matches: result[:files_with_matches],
253
+ files_searched: result[:files_searched],
254
+ truncated: result[:truncated],
255
+ truncation_reason: result[:truncation_reason]
256
+ }
257
+ }
258
+
259
+ # Include list of files with match counts
260
+ if result[:results] && !result[:results].empty?
261
+ compact[:files] = result[:results].map do |file_result|
262
+ {
263
+ file: file_result[:file],
264
+ match_count: file_result[:matches].length
265
+ }
266
+ end
267
+
268
+ # Include sample matches (first 2 matches from first 3 files) for context
269
+ sample_results = result[:results].take(3)
270
+ compact[:sample_matches] = sample_results.map do |file_result|
271
+ {
272
+ file: file_result[:file],
273
+ matches: file_result[:matches].take(2).map do |match|
274
+ {
275
+ line_number: match[:line_number],
276
+ line: match[:line]
277
+ # Omit context to save space - it's rarely needed by LLM
278
+ }
279
+ end
280
+ }
281
+ end
282
+ end
283
+
284
+ compact
285
+ end
286
+
287
+
288
+ def search_file(file, regex, context_lines, max_matches)
289
+ matches = []
290
+
291
+ # Use File.foreach for memory-efficient line-by-line reading.
292
+ # Scrub invalid UTF-8 bytes so results survive JSON encoding.
293
+ File.foreach(file, chomp: true).with_index do |raw_line, index|
294
+ line = safe_utf8(raw_line)
295
+ # Stop if we have enough matches for this file
296
+ break if matches.length >= max_matches
297
+
298
+ next unless line.match?(regex)
299
+
300
+ # Truncate long lines
301
+ display_line = line.length > MAX_LINE_LENGTH ? "#{line[0...MAX_LINE_LENGTH]}..." : line
302
+
303
+ # Get context if requested
304
+ if context_lines > 0
305
+ context = get_line_context(file, index, context_lines)
306
+ else
307
+ context = nil
308
+ end
309
+
310
+ matches << {
311
+ line_number: index + 1,
312
+ line: display_line,
313
+ context: context
314
+ }
315
+ end
316
+
317
+ matches
318
+ rescue StandardError
319
+ []
320
+ end
321
+
322
+ # Get context lines around a match
323
+ def get_line_context(file, match_index, context_lines)
324
+ lines = File.readlines(file, chomp: true).map! { |l| safe_utf8(l) }
325
+ start_line = [0, match_index - context_lines].max
326
+ end_line = [lines.length - 1, match_index + context_lines].min
327
+
328
+ context = []
329
+ (start_line..end_line).each do |i|
330
+ line_content = lines[i]
331
+ # Truncate long lines in context too
332
+ display_content = line_content.length > MAX_LINE_LENGTH ?
333
+ "#{line_content[0...MAX_LINE_LENGTH]}..." :
334
+ line_content
335
+
336
+ context << {
337
+ line_number: i + 1,
338
+ content: display_content,
339
+ is_match: i == match_index
340
+ }
341
+ end
342
+
343
+ context
344
+ rescue StandardError
345
+ nil
346
+ end
347
+
348
+ # Scrub invalid UTF-8 byte sequences (see file_reader.rb for rationale).
349
+ private def safe_utf8(str)
350
+ return str if str.nil?
351
+ return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
352
+ str.encode("UTF-8", invalid: :replace, undef: :replace, replace: "\u{FFFD}")
353
+ end
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ module Tools
5
+ # Tool for invoking skills within the agent
6
+ # This allows the AI to call skills as tools rather than requiring explicit user commands
7
+ class InvokeSkill < Base
8
+ self.tool_name = "invoke_skill"
9
+ self.tool_description = "Invoke a specialized skill to handle specific tasks. Use this when user's request matches a skill's description (e.g., code exploration, document creation, etc.). This will read the skill's instructions and execute them appropriately (either inline or in a subagent)."
10
+ self.tool_category = "skill_management"
11
+ self.tool_parameters = {
12
+ type: "object",
13
+ properties: {
14
+ skill_name: {
15
+ type: "string",
16
+ description: "Name of the skill to invoke (e.g., 'code-explorer', 'pptx', 'pdf')"
17
+ },
18
+ task: {
19
+ type: "string",
20
+ description: "The task or query to pass to the skill"
21
+ }
22
+ },
23
+ required: ["skill_name", "task"]
24
+ }
25
+
26
+ # Execute the skill invocation
27
+ # @param skill_name [String] Name of the skill to invoke
28
+ # @param task [String] Task description to pass to the skill
29
+ # @param agent [Octo::Agent] Agent instance (injected)
30
+ # @param skill_loader [Octo::SkillLoader] Skill loader instance (injected)
31
+ # @return [Hash] Result of skill execution
32
+ def execute(skill_name:, task:, agent: nil, skill_loader: nil, working_dir: nil)
33
+ # Validate injected dependencies
34
+ return { error: "Agent context is required" } unless agent
35
+ return { error: "Skill loader is required" } unless skill_loader
36
+
37
+ # Find skill by name
38
+ skill = skill_loader.find_by_name(skill_name)
39
+ return { error: "Skill not found: #{skill_name}" } unless skill
40
+
41
+ # Execute skill based on its configuration.
42
+ # Note: disable-model-invocation only prevents the skill from appearing in AVAILABLE SKILLS
43
+ # (so the model won't auto-discover it). It does NOT block execution here — the user may
44
+ # have triggered this skill explicitly via a slash command (/skill-name).
45
+ if skill.fork_agent?
46
+ # Execute in isolated subagent
47
+ result = agent.send(:execute_skill_with_subagent, skill, task)
48
+ {
49
+ message: "Skill '#{skill_name}' executed in subagent",
50
+ result: result,
51
+ skill_type: "subagent"
52
+ }
53
+ else
54
+ # Deferred injection path: enqueue the skill inject on the agent.
55
+ #
56
+ # Injecting inside execute() would produce an illegal message ordering for Bedrock:
57
+ # assistant: {toolUse: invoke_skill}
58
+ # assistant: {text: skill_instructions} ← injected here (breaks pairing)
59
+ # user: {toolResult: invoke_skill} ← observe() appends this too late
60
+ #
61
+ # Instead, enqueue the injection so the agent loop can flush it AFTER observe()
62
+ # appends the toolResult, producing the correct sequence:
63
+ # assistant: {toolUse: invoke_skill}
64
+ # user: {toolResult: ...} ← observe() appends first
65
+ # assistant: {text: skill_instructions} ← flush_pending_injections runs here
66
+ # user: "[SYSTEM] please proceed"
67
+ agent.enqueue_injection(skill, task)
68
+ "Skill '#{skill_name}' instructions expanded. Proceed to execute the task."
69
+ end
70
+ end
71
+
72
+ # Format the tool call for display
73
+ # @param args [Hash] Tool arguments
74
+ # @return [String] Formatted call description
75
+ def format_call(args)
76
+ skill = args[:skill_name] || args["skill_name"]
77
+ "InvokeSkill(#{skill})"
78
+ end
79
+
80
+ # Format the tool result for display
81
+ # @param result [Hash] Tool execution result
82
+ # @return [String] Formatted result summary
83
+ def format_result(result)
84
+ if result.is_a?(String)
85
+ result
86
+ elsif result[:error]
87
+ "Error: #{result[:error]}"
88
+ elsif result[:skill_type] == "subagent"
89
+ "Subagent executed successfully"
90
+ else
91
+ "Skill content expanded"
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ module Tools
5
+ # Tool for listing task history (Time Machine feature)
6
+ class ListTasks < Base
7
+ self.tool_name = "list_tasks"
8
+ self.tool_description = "List recent tasks in the task history with summaries. " \
9
+ "Shows current task, past tasks, and future tasks (after undo). " \
10
+ "Use when user wants to see task history or choose which task to undo/redo to."
11
+ self.tool_category = "time_machine"
12
+ self.tool_parameters = {
13
+ type: "object",
14
+ properties: {
15
+ limit: {
16
+ type: "integer",
17
+ description: "Maximum number of recent tasks to show (default: 10)",
18
+ default: 10
19
+ }
20
+ }
21
+ }
22
+
23
+ def execute(agent:, limit: 10, **_args)
24
+ history = agent.get_task_history(limit: limit)
25
+
26
+ if history.empty?
27
+ return "No task history available."
28
+ end
29
+
30
+ lines = ["Task History:"]
31
+ history.each do |task|
32
+ indicator = case task[:status]
33
+ when :current then "→"
34
+ when :past then " "
35
+ when :future then "↯"
36
+ end
37
+
38
+ branch_indicator = task[:has_branches] ? " ⎇" : ""
39
+ lines << "#{indicator}#{branch_indicator} Task #{task[:task_id]}: #{task[:summary]}"
40
+ end
41
+
42
+ lines.join("\n")
43
+ end
44
+
45
+ def format_call(limit: 10, **_args)
46
+ "Listing task history (limit: #{limit})..."
47
+ end
48
+
49
+ def format_result(result)
50
+ result
51
+ end
52
+ end
53
+ end
54
+ end