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,388 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require_relative "base"
6
+ require_relative "../utils/trash_directory"
7
+
8
+ module Octo
9
+ module Tools
10
+ class TrashManager < Base
11
+ self.tool_name = "trash_manager"
12
+ self.tool_description = "Manage deleted files in the AI trash - list, restore, or permanently delete files"
13
+ self.tool_category = "system"
14
+ self.tool_parameters = {
15
+ type: "object",
16
+ properties: {
17
+ action: {
18
+ type: "string",
19
+ enum: ["list", "restore", "status", "empty", "help"],
20
+ description: "Action to perform: 'list' (show deleted files), 'restore' (restore a file), 'status' (show trash summary), 'empty' (permanently delete old files), 'help' (show usage)"
21
+ },
22
+ file_path: {
23
+ type: "string",
24
+ description: "Original file path to restore (required for 'restore' action)"
25
+ },
26
+ days_old: {
27
+ type: "integer",
28
+ description: "For 'empty' action: permanently delete files older than this many days (default: 7)"
29
+ }
30
+ },
31
+ required: ["action"]
32
+ }
33
+
34
+ def execute(action:, file_path: nil, days_old: 7, working_dir: nil)
35
+ project_root = working_dir || Dir.pwd
36
+
37
+ # Use global trash directory organized by project
38
+ trash_directory = Octo::TrashDirectory.new(project_root)
39
+ trash_dir = trash_directory.trash_dir
40
+
41
+ unless Dir.exist?(trash_dir)
42
+ return {
43
+ action: action,
44
+ success: false,
45
+ message: "No trash directory found. No files have been safely deleted yet."
46
+ }
47
+ end
48
+
49
+ case action.downcase
50
+ when 'list'
51
+ list_deleted_files(trash_dir, project_root)
52
+ when 'restore'
53
+ return { action: action, success: false, message: "file_path is required for restore action" } unless file_path
54
+ restore_file(trash_dir, file_path, project_root)
55
+ when 'status'
56
+ show_trash_status(trash_dir, project_root)
57
+ when 'empty'
58
+ empty_trash(trash_dir, days_old, project_root)
59
+ when 'help'
60
+ show_help
61
+ else
62
+ { action: action, success: false, message: "Unknown action: #{action}" }
63
+ end
64
+ end
65
+
66
+ def list_deleted_files(trash_dir, project_root)
67
+ deleted_files = get_deleted_files(trash_dir, project_root)
68
+
69
+ if deleted_files.empty?
70
+ return {
71
+ action: 'list',
72
+ success: true,
73
+ count: 0,
74
+ message: "🗑️ Trash is empty"
75
+ }
76
+ end
77
+
78
+ file_list = deleted_files.map.with_index(1) do |file, index|
79
+ size_info = file[:file_size] ? " (#{format_bytes(file[:file_size])})" : ""
80
+ "#{index}. #{file[:original_path]}#{size_info}\n Deleted: #{format_time(file[:deleted_at])}"
81
+ end
82
+
83
+ {
84
+ action: 'list',
85
+ success: true,
86
+ count: deleted_files.size,
87
+ files: deleted_files,
88
+ message: "🗑️ Deleted Files:\n\n#{file_list.join("\n\n")}\n\n💡 Use trash_manager with action='restore' and file_path='<path>' to restore a file"
89
+ }
90
+ end
91
+
92
+ def restore_file(trash_dir, file_path, project_root)
93
+ deleted_files = get_deleted_files(trash_dir, project_root)
94
+ expanded_path = File.expand_path(file_path, project_root)
95
+
96
+ target_file = deleted_files.find { |f| f[:original_path] == expanded_path }
97
+
98
+ unless target_file
99
+ similar_files = deleted_files.select { |f| File.basename(f[:original_path]) == File.basename(file_path) }
100
+
101
+ if similar_files.any?
102
+ suggestions = similar_files.map { |f| f[:original_path] }.join("\n - ")
103
+ return {
104
+ action: 'restore',
105
+ success: false,
106
+ message: "File not found in trash: #{file_path}\n\nDid you mean one of these?\n - #{suggestions}"
107
+ }
108
+ else
109
+ return {
110
+ action: 'restore',
111
+ success: false,
112
+ message: "File not found in trash: #{file_path}\n\nUse trash_manager with action='list' to see available files."
113
+ }
114
+ end
115
+ end
116
+
117
+ if File.exist?(expanded_path)
118
+ return {
119
+ action: 'restore',
120
+ success: false,
121
+ message: "Cannot restore: file already exists at #{file_path}"
122
+ }
123
+ end
124
+
125
+ begin
126
+ # Ensure target directory exists
127
+ FileUtils.mkdir_p(File.dirname(expanded_path))
128
+
129
+ # Restore file
130
+ FileUtils.mv(target_file[:trash_file], expanded_path)
131
+ File.delete("#{target_file[:trash_file]}.metadata.json")
132
+
133
+ {
134
+ action: 'restore',
135
+ success: true,
136
+ restored_file: expanded_path,
137
+ message: "✅ Successfully restored: #{file_path}"
138
+ }
139
+ rescue StandardError => e
140
+ {
141
+ action: 'restore',
142
+ success: false,
143
+ message: "❌ Failed to restore file: #{e.message}"
144
+ }
145
+ end
146
+ end
147
+
148
+ def show_trash_status(trash_dir, project_root)
149
+ deleted_files = get_deleted_files(trash_dir, project_root)
150
+ total_size = deleted_files.sum { |f| f[:file_size] || 0 }
151
+
152
+ if deleted_files.empty?
153
+ return {
154
+ action: 'status',
155
+ success: true,
156
+ count: 0,
157
+ total_size: 0,
158
+ message: "🗑️ Trash is empty"
159
+ }
160
+ end
161
+
162
+ # Group by file type
163
+ by_type = deleted_files.group_by { |f| f[:file_type] || 'no extension' }
164
+ type_summary = by_type.map do |ext, files|
165
+ size = files.sum { |f| f[:file_size] || 0 }
166
+ " #{ext}: #{files.size} files (#{format_bytes(size)})"
167
+ end.join("\n")
168
+
169
+ recent_files = deleted_files.first(3).map do |file|
170
+ " - #{File.basename(file[:original_path])} (#{format_time(file[:deleted_at])})"
171
+ end.join("\n")
172
+
173
+ message = []
174
+ message << "🗑️ Trash Status:"
175
+ message << " Files: #{deleted_files.count}"
176
+ message << " Total size: #{format_bytes(total_size)}"
177
+ message << " Location: #{trash_dir}"
178
+ message << ""
179
+ message << "📊 By file type:"
180
+ message << type_summary
181
+ message << ""
182
+ message << "📅 Recently deleted:"
183
+ message << recent_files
184
+
185
+ {
186
+ action: 'status',
187
+ success: true,
188
+ count: deleted_files.size,
189
+ total_size: total_size,
190
+ by_type: by_type.transform_values(&:size),
191
+ message: message.join("\n")
192
+ }
193
+ end
194
+
195
+ def empty_trash(trash_dir, days_old, project_root)
196
+ deleted_files = get_deleted_files(trash_dir, project_root)
197
+ cutoff_time = Time.now - (days_old * 24 * 60 * 60)
198
+
199
+ old_files = deleted_files.select do |file|
200
+ Time.parse(file[:deleted_at]) < cutoff_time
201
+ end
202
+
203
+ if old_files.empty?
204
+ return {
205
+ action: 'empty',
206
+ success: true,
207
+ deleted_count: 0,
208
+ message: "🗑️ No files older than #{days_old} days found in trash"
209
+ }
210
+ end
211
+
212
+ deleted_count = 0
213
+ freed_size = 0
214
+
215
+ old_files.each do |file|
216
+ begin
217
+ if File.exist?(file[:trash_file])
218
+ if File.directory?(file[:trash_file])
219
+ freed_size += _dir_size(file[:trash_file])
220
+ FileUtils.rm_rf(file[:trash_file])
221
+ else
222
+ freed_size += File.size(file[:trash_file])
223
+ File.delete(file[:trash_file])
224
+ end
225
+ deleted_count += 1
226
+ end
227
+ File.delete("#{file[:trash_file]}.metadata.json") if File.exist?("#{file[:trash_file]}.metadata.json")
228
+ rescue StandardError => e
229
+ # Continue processing other files, but log the error
230
+ end
231
+ end
232
+
233
+ {
234
+ action: 'empty',
235
+ success: true,
236
+ deleted_count: deleted_count,
237
+ freed_size: freed_size,
238
+ days_old: days_old,
239
+ message: "🗑️ Permanently deleted #{deleted_count} files older than #{days_old} days\n💾 Freed up #{format_bytes(freed_size)} of disk space"
240
+ }
241
+ end
242
+
243
+ def show_help
244
+ help_text = <<~HELP
245
+ 🗑️ Trash Manager Help
246
+
247
+ The `terminal` tool automatically moves deleted files to a trash directory
248
+ instead of permanently deleting them. This tool helps you manage those files.
249
+
250
+ Available actions:
251
+
252
+ 📋 list - Show all deleted files
253
+ Example: trash_manager(action="list")
254
+
255
+ ♻️ restore - Restore a deleted file to its original location
256
+ Example: trash_manager(action="restore", file_path="path/to/file.txt")
257
+
258
+ 📊 status - Show trash summary with statistics
259
+ Example: trash_manager(action="status")
260
+
261
+ 🗑️ empty - Permanently delete files older than N days (default: 7)
262
+ Example: trash_manager(action="empty", days_old=7)
263
+
264
+ ❓ help - Show this help message
265
+
266
+ 💡 Tips:
267
+ - Use 'list' to see what files are in trash
268
+ - Use 'restore' to get back accidentally deleted files
269
+ - Use 'empty' periodically to free up disk space
270
+ - All deletions by `terminal` are logged in ~/.octo/safety_logs/
271
+ HELP
272
+
273
+ {
274
+ action: 'help',
275
+ success: true,
276
+ message: help_text
277
+ }
278
+ end
279
+
280
+ def get_deleted_files(trash_dir, project_root)
281
+ deleted_files = []
282
+
283
+ Dir.glob(File.join(trash_dir, "*.metadata.json")).each do |metadata_file|
284
+ begin
285
+ metadata = JSON.parse(File.read(metadata_file))
286
+ trash_file = metadata_file.sub('.metadata.json', '')
287
+
288
+ # Only include existing trash files
289
+ if File.exist?(trash_file)
290
+ deleted_files << {
291
+ original_path: metadata['original_path'],
292
+ deleted_at: metadata['deleted_at'],
293
+ trash_file: trash_file,
294
+ file_size: metadata['file_size'],
295
+ file_type: metadata['file_type'],
296
+ file_mode: metadata['file_mode']
297
+ }
298
+ end
299
+ rescue StandardError
300
+ # Skip corrupted metadata files
301
+ end
302
+ end
303
+
304
+ deleted_files.sort_by { |f| f[:deleted_at] }.reverse
305
+ end
306
+
307
+ private def _dir_size(dir)
308
+ total = 0
309
+ Find.find(dir) do |path|
310
+ total += File.size(path) if File.file?(path)
311
+ end
312
+ total
313
+ rescue StandardError
314
+ 0
315
+ end
316
+
317
+ def format_bytes(bytes)
318
+ return "0 B" if bytes.zero?
319
+
320
+ units = %w[B KB MB GB]
321
+ unit_index = 0
322
+ size = bytes.to_f
323
+
324
+ while size >= 1024 && unit_index < units.length - 1
325
+ size /= 1024.0
326
+ unit_index += 1
327
+ end
328
+
329
+ if unit_index == 0
330
+ "#{size.to_i} #{units[unit_index]}"
331
+ else
332
+ "#{size.round(2)} #{units[unit_index]}"
333
+ end
334
+ end
335
+
336
+ def format_time(time_str)
337
+ time = Time.parse(time_str)
338
+ if time.to_date == Date.today
339
+ time.strftime("%H:%M")
340
+ elsif time.to_date == Date.today - 1
341
+ "yesterday #{time.strftime('%H:%M')}"
342
+ elsif time.year == Date.today.year
343
+ time.strftime("%m/%d %H:%M")
344
+ else
345
+ time.strftime("%Y/%m/%d")
346
+ end
347
+ rescue
348
+ time_str
349
+ end
350
+
351
+ def format_call(args)
352
+ action = args[:action] || args['action'] || 'unknown'
353
+ "TrashManager(#{action})"
354
+ end
355
+
356
+ def format_result(result)
357
+ action = result[:action] || 'unknown'
358
+ success = result[:success]
359
+
360
+ case action
361
+ when 'list'
362
+ count = result[:count] || 0
363
+ "📋 Listed #{count} deleted files"
364
+ when 'restore'
365
+ if success
366
+ "♻️ File restored successfully"
367
+ else
368
+ "❌ Restore failed"
369
+ end
370
+ when 'status'
371
+ count = result[:count] || 0
372
+ "📊 Trash: #{count} files"
373
+ when 'empty'
374
+ if success
375
+ deleted_count = result[:deleted_count] || 0
376
+ "🗑️ Emptied #{deleted_count} files"
377
+ else
378
+ "❌ Empty failed"
379
+ end
380
+ when 'help'
381
+ "❓ Help displayed"
382
+ else
383
+ success ? "[OK] #{action} completed" : "[Error] #{action} failed"
384
+ end
385
+ end
386
+ end
387
+ end
388
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ module Tools
5
+ # Tool for undoing the last task (Time Machine feature)
6
+ class UndoTask < Base
7
+ self.tool_name = "undo_task"
8
+ self.tool_description = "Undo the last task and restore files to previous state. " \
9
+ "Use when user wants to go back to previous state or undo recent changes."
10
+ self.tool_category = "time_machine"
11
+ self.tool_parameters = {
12
+ type: "object",
13
+ properties: {}
14
+ }
15
+
16
+ def execute(agent:, **_args)
17
+ result = agent.undo_last_task
18
+
19
+ if result[:success]
20
+ result[:message]
21
+ else
22
+ "Error: #{result[:message]}"
23
+ end
24
+ end
25
+
26
+ def format_call(**_args)
27
+ "Undoing last task..."
28
+ end
29
+
30
+ def format_result(result)
31
+ result
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "tmpdir"
6
+ require "fileutils"
7
+ require_relative "../utils/encoding"
8
+
9
+ module Octo
10
+ module Tools
11
+ class WebFetch < Base
12
+ self.tool_name = "web_fetch"
13
+ self.tool_description = "Fetch and parse content from a web page. Returns the page content, title, and metadata."
14
+ self.tool_category = "web"
15
+ self.tool_parameters = {
16
+ type: "object",
17
+ properties: {
18
+ url: {
19
+ type: "string",
20
+ description: "The URL to fetch (must be a valid HTTP/HTTPS URL)"
21
+ },
22
+ max_length: {
23
+ type: "integer",
24
+ description: "Maximum content length to return in characters (default: 3000)",
25
+ default: 3000
26
+ }
27
+ },
28
+ required: %w[url]
29
+ }
30
+
31
+ def execute(url:, max_length: 3000, working_dir: nil)
32
+ # Validate URL
33
+ begin
34
+ uri = URI.parse(url)
35
+ unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
36
+ return { error: "URL must be HTTP or HTTPS" }
37
+ end
38
+ rescue URI::InvalidURIError => e
39
+ return { error: "Invalid URL: #{e.message}" }
40
+ end
41
+
42
+ begin
43
+ # Fetch the web page
44
+ response = fetch_url(uri)
45
+
46
+ # Extract content and force UTF-8 encoding at the source
47
+ content = Octo::Utils::Encoding.to_utf8(response.body)
48
+ content_type = response["content-type"] || ""
49
+
50
+ # Parse HTML if it's an HTML page
51
+ if content_type.include?("text/html")
52
+ result = parse_html(content, max_length, url)
53
+ result[:url] = url
54
+ result[:content_type] = content_type
55
+ result[:status_code] = response.code.to_i
56
+ result[:error] = nil
57
+ result
58
+ else
59
+ # For non-HTML content, return raw text
60
+ result = handle_raw_content(content, max_length, url, content_type, response.code.to_i)
61
+ result
62
+ end
63
+ rescue StandardError => e
64
+ { error: "Failed to fetch URL: #{e.message}" }
65
+ end
66
+ end
67
+
68
+ def handle_raw_content(content, max_length, url, content_type, status_code)
69
+ truncated = content.length > max_length
70
+ temp_file = nil
71
+
72
+ if truncated
73
+ temp_dir = Dir.mktmpdir
74
+ domain = extract_domain(url)
75
+ safe_name = domain.gsub(/[^\w\-.]/, '_')[0...50]
76
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
77
+ temp_file = File.join(temp_dir, "#{safe_name}_#{timestamp}.txt")
78
+ File.write(temp_file, content)
79
+ end
80
+
81
+ {
82
+ url: url,
83
+ content_type: content_type,
84
+ status_code: status_code,
85
+ content: content[0, max_length],
86
+ truncated: truncated,
87
+ temp_file: temp_file,
88
+ error: nil
89
+ }
90
+ end
91
+
92
+ def extract_domain(url)
93
+ uri = URI.parse(url)
94
+ uri.host || url.gsub(/[^\w\-.]/, '_')
95
+ rescue
96
+ url.gsub(/[^\w\-.]/, '_')
97
+ end
98
+
99
+ def fetch_url(uri)
100
+ # Follow redirects (max 5)
101
+ redirects = 0
102
+ max_redirects = 5
103
+
104
+ loop do
105
+ request = Net::HTTP::Get.new(uri)
106
+ request["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
107
+ request["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
108
+ request["Accept-Language"] = "zh-CN,zh;q=0.9,en;q=0.8"
109
+
110
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: 15) do |http|
111
+ http.request(request)
112
+ end
113
+
114
+ case response
115
+ when Net::HTTPSuccess
116
+ return response
117
+ when Net::HTTPRedirection
118
+ redirects += 1
119
+ raise "Too many redirects" if redirects > max_redirects
120
+
121
+ location = response["location"]
122
+ new_uri = URI.parse(location)
123
+ # Handle relative redirects by merging with the current URI
124
+ uri = new_uri.relative? ? uri.merge(new_uri) : new_uri
125
+ else
126
+ raise "HTTP error: #{response.code} #{response.message}"
127
+ end
128
+ end
129
+ end
130
+
131
+ def parse_html(html, max_length, url = nil)
132
+ # Extract title
133
+ title = ""
134
+ if html =~ %r{<title[^>]*>(.*?)</title>}mi
135
+ title = $1.strip.gsub(/\s+/, " ")
136
+ end
137
+
138
+ # Extract meta description
139
+ description = ""
140
+ if html =~ /<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["']/mi
141
+ description = $1.strip
142
+ elsif html =~ /<meta[^>]*content=["']([^"']*)["'][^>]*name=["']description["']/mi
143
+ description = $1.strip
144
+ end
145
+
146
+ # Remove script and style tags
147
+ text = html.gsub(%r{<script[^>]*>.*?</script>}mi, "")
148
+ .gsub(%r{<style[^>]*>.*?</style>}mi, "")
149
+
150
+ # Remove HTML tags
151
+ text = text.gsub(/<[^>]+>/, " ")
152
+
153
+ # Clean up whitespace
154
+ text = text.gsub(/\s+/, " ").strip
155
+
156
+ # Check if we need to save to temp file
157
+ truncated = text.length > max_length
158
+ temp_file = nil
159
+
160
+ if truncated
161
+ temp_dir = Dir.mktmpdir
162
+ domain = url ? extract_domain(url) : "web_fetch"
163
+ safe_name = domain.gsub(/[^\w\-.]/, '_')[0...50]
164
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
165
+ temp_file = File.join(temp_dir, "#{safe_name}_#{timestamp}.txt")
166
+ File.write(temp_file, text)
167
+ end
168
+
169
+ {
170
+ title: title,
171
+ description: description,
172
+ content: text[0, max_length],
173
+ truncated: truncated,
174
+ temp_file: temp_file
175
+ }
176
+ end
177
+
178
+ def format_call(args)
179
+ url = args[:url] || args['url'] || ''
180
+ # Extract domain from URL for display
181
+ begin
182
+ uri = URI.parse(url)
183
+ domain = uri.host || url
184
+ "web_fetch(#{domain})"
185
+ rescue
186
+ display_url = url.length > 40 ? "#{url[0..37]}..." : url
187
+ "web_fetch(\"#{display_url}\")"
188
+ end
189
+ end
190
+
191
+ def format_result(result)
192
+ if result[:error]
193
+ "[Error] #{result[:error]}"
194
+ else
195
+ title = result[:title] || 'Untitled'
196
+ display_title = title.length > 40 ? "#{title[0..37]}..." : title
197
+ "[OK] Fetched: #{display_title}"
198
+ end
199
+ end
200
+
201
+ def format_result_for_ui(result)
202
+ return nil if result[:error]
203
+ {
204
+ type: "web_fetch",
205
+ url: result[:url],
206
+ title: result[:title],
207
+ content_preview: result[:content]&.[](0, 500),
208
+ truncated: result[:truncated] || false,
209
+ status_code: result[:status_code]
210
+ }
211
+ end
212
+
213
+ # Format result for LLM consumption - return compact version to save tokens
214
+ def format_result_for_llm(result)
215
+ # Return error as-is
216
+ return result if result[:error]
217
+
218
+ # Build compact result
219
+ compact = {
220
+ url: result[:url],
221
+ title: result[:title],
222
+ description: result[:description],
223
+ status_code: result[:status_code]
224
+ }
225
+
226
+ # Add truncated notice and temp file info if content was truncated
227
+ if result[:truncated] && result[:temp_file]
228
+ compact[:content] = result[:content]
229
+ compact[:truncated] = true
230
+ compact[:temp_file] = result[:temp_file]
231
+ compact[:message] = "[Content truncated - full content saved to: #{result[:temp_file]}. " \
232
+ "Use grep to search keywords, or file_reader with start_line/end_line to read sections.]"
233
+ else
234
+ compact[:content] = result[:content]
235
+ compact[:truncated] = result[:truncated] || false
236
+ end
237
+
238
+ compact
239
+ end
240
+ end
241
+ end
242
+ end