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,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ # Parser for .gitignore files to determine which files should be ignored
5
+ class GitignoreParser
6
+ attr_reader :patterns
7
+
8
+ def initialize(gitignore_path = nil)
9
+ @patterns = []
10
+ @negation_patterns = []
11
+
12
+ if gitignore_path && File.exist?(gitignore_path)
13
+ parse_gitignore(gitignore_path)
14
+ end
15
+ end
16
+
17
+ def merge!(other_gitignore_path, prefix: nil)
18
+ return unless other_gitignore_path && File.exist?(other_gitignore_path)
19
+
20
+ File.readlines(other_gitignore_path, chomp: true).each do |line|
21
+ next if line.strip.empty? || line.start_with?('#')
22
+
23
+ negation = line.start_with?('!')
24
+ raw = negation ? line[1..] : line
25
+ info = normalize_pattern(raw)
26
+
27
+ if prefix
28
+ original = info[:pattern]
29
+ original = original[1..] if info[:is_absolute]
30
+ info[:pattern] = "#{prefix}/#{original}"
31
+ info[:is_absolute] = false
32
+ end
33
+
34
+ if negation
35
+ @negation_patterns << info
36
+ else
37
+ @patterns << info
38
+ end
39
+ end
40
+ rescue StandardError => e
41
+ warn "Warning: Failed to merge .gitignore: #{e.message}"
42
+ end
43
+
44
+ # Check if a file path should be ignored
45
+ def ignored?(path)
46
+ relative_path = path.start_with?('./') ? path[2..] : path
47
+
48
+ # Check negation patterns first (! prefix in .gitignore)
49
+ @negation_patterns.each do |pattern|
50
+ return false if match_pattern?(relative_path, pattern)
51
+ end
52
+
53
+ # Then check ignore patterns
54
+ @patterns.each do |pattern|
55
+ return true if match_pattern?(relative_path, pattern)
56
+ end
57
+
58
+ false
59
+ end
60
+
61
+
62
+ def parse_gitignore(path)
63
+ File.readlines(path, chomp: true).each do |line|
64
+ # Skip comments and empty lines
65
+ next if line.strip.empty? || line.start_with?('#')
66
+
67
+ # Handle negation patterns (lines starting with !)
68
+ if line.start_with?('!')
69
+ @negation_patterns << normalize_pattern(line[1..])
70
+ else
71
+ @patterns << normalize_pattern(line)
72
+ end
73
+ end
74
+ rescue StandardError => e
75
+ # If we can't parse .gitignore, just continue with empty patterns
76
+ warn "Warning: Failed to parse .gitignore: #{e.message}"
77
+ end
78
+
79
+ def normalize_pattern(pattern)
80
+ pattern = pattern.strip
81
+
82
+ # Remove trailing whitespace
83
+ pattern = pattern.rstrip
84
+
85
+ # Store original for directory detection
86
+ is_directory = pattern.end_with?('/')
87
+ pattern = pattern.chomp('/')
88
+
89
+ {
90
+ pattern: pattern,
91
+ is_directory: is_directory,
92
+ is_absolute: pattern.start_with?('/'),
93
+ has_wildcard: pattern.include?('*') || pattern.include?('?'),
94
+ has_double_star: pattern.include?('**')
95
+ }
96
+ end
97
+
98
+ def match_pattern?(path, pattern_info)
99
+ pattern = pattern_info[:pattern]
100
+ is_absolute = pattern_info[:is_absolute]
101
+
102
+ # For absolute patterns (starting with /), remove the leading slash
103
+ # These patterns match from the root of the repository
104
+ if is_absolute
105
+ pattern = pattern[1..]
106
+ # Absolute patterns match exactly from the start of the path
107
+ return true if path == pattern
108
+ return true if path.start_with?("#{pattern}/")
109
+ end
110
+
111
+ # Handle directory patterns
112
+ if pattern_info[:is_directory]
113
+ # Directory patterns should match the directory and all its contents
114
+ return true if path == pattern
115
+ return true if path.start_with?("#{pattern}/")
116
+ # Also check if any path component matches the directory pattern
117
+ return true if path.split('/').include?(pattern)
118
+ end
119
+
120
+ # Handle different wildcard patterns
121
+ if pattern_info[:has_double_star]
122
+ # Convert ** to match any number of directories
123
+ regex_pattern = Regexp.escape(pattern)
124
+ .gsub('\*\*/', '(.*/)?') # **/ matches zero or more directories
125
+ .gsub('\*\*', '.*') # ** at end matches anything
126
+ .gsub('\*', '[^/]*') # * matches anything except /
127
+ .gsub('\?', '[^/]') # ? matches single character except /
128
+
129
+ regex = Regexp.new("^#{regex_pattern}$")
130
+ return true if path.match?(regex)
131
+ return true if path.split('/').any? { |part| part.match?(regex) }
132
+ elsif pattern_info[:has_wildcard]
133
+ # Convert glob pattern to regex
134
+ regex_pattern = Regexp.escape(pattern)
135
+ .gsub('\*', '[^/]*')
136
+ .gsub('\?', '[^/]')
137
+
138
+ regex = Regexp.new("^#{regex_pattern}$")
139
+ return true if path.match?(regex)
140
+ return true if File.basename(path).match?(regex)
141
+ else
142
+ # Exact match - pattern without wildcards
143
+ # Match as basename or as path prefix
144
+ return true if path == pattern
145
+ return true if path.start_with?("#{pattern}/")
146
+ return true if File.basename(path) == pattern
147
+ # Also check if pattern matches any path component
148
+ return true if path.split('/').include?(pattern)
149
+ end
150
+
151
+ false
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ module Utils
5
+ # Auto-rolling fixed-size array.
6
+ # Automatically discards oldest elements when the line-count limit is exceeded.
7
+ #
8
+ # Optional limits (all default to nil = no limit):
9
+ # max_line_chars – truncate each individual line to this many characters on push
10
+ # max_chars – once the total accepted chars reach this threshold, further
11
+ # pushes are silently dropped (sets #truncated? = true)
12
+ #
13
+ # These extra limits are fully opt-in; existing callers that only pass max_size
14
+ # are completely unaffected.
15
+ class LimitStack
16
+ attr_reader :max_size, :items
17
+
18
+ def initialize(max_size: 5000, max_line_chars: nil, max_chars: nil)
19
+ @max_size = max_size
20
+ @max_line_chars = max_line_chars
21
+ @max_chars = max_chars
22
+
23
+ @items = []
24
+ @total_chars = 0 # chars currently stored in @items
25
+ @truncated = false
26
+ @chars_full = false # latched true once max_chars is reached
27
+ end
28
+
29
+ # True if any content was dropped (lines rolled off the front OR
30
+ # chars budget was exceeded OR a line was truncated).
31
+ def truncated?
32
+ @truncated
33
+ end
34
+
35
+ # Add elements (supports single or multiple)
36
+ def push(*elements)
37
+ elements.each do |element|
38
+ _push_one(element)
39
+ end
40
+ self
41
+ end
42
+ alias_method :<<, :push
43
+
44
+ # Add multi-line text (split by lines and add)
45
+ def push_lines(text)
46
+ return self if text.nil? || text.empty?
47
+
48
+ lines = text.is_a?(Array) ? text : text.lines
49
+ lines.each { |line| _push_one(line) }
50
+ self
51
+ end
52
+
53
+ # Remove and return the last element
54
+ def pop
55
+ item = @items.pop
56
+ @total_chars -= item.length if item.is_a?(String)
57
+ item
58
+ end
59
+
60
+ # Get last N elements
61
+ def last(n = nil)
62
+ n ? @items.last(n) : @items.last
63
+ end
64
+
65
+ # Get all elements
66
+ def to_a
67
+ @items.dup
68
+ end
69
+
70
+ # Convert to string (for text content)
71
+ def to_s
72
+ @items.join
73
+ end
74
+
75
+ # Current size
76
+ def size
77
+ @items.size
78
+ end
79
+
80
+ # Check if empty
81
+ def empty?
82
+ @items.empty?
83
+ end
84
+
85
+ # Clear all elements
86
+ def clear
87
+ @items.clear
88
+ @total_chars = 0
89
+ @truncated = false
90
+ @chars_full = false
91
+ self
92
+ end
93
+
94
+ # Iterate over elements
95
+ def each(&block)
96
+ @items.each(&block)
97
+ end
98
+
99
+ # kept for compatibility (called internally; public so subclasses can override)
100
+ def trim_if_needed
101
+ while @items.size > @max_size
102
+ removed = @items.shift
103
+ @total_chars -= removed.length if removed.is_a?(String)
104
+ @truncated = true
105
+ end
106
+ end
107
+
108
+ private def _push_one(element)
109
+ # --- chars budget check ---
110
+ if @chars_full
111
+ @truncated = true
112
+ return
113
+ end
114
+
115
+ item = element
116
+
117
+ # --- per-line truncation ---
118
+ if @max_line_chars && item.is_a?(String) && item.length > @max_line_chars
119
+ item = item[0, @max_line_chars]
120
+ # Preserve trailing newline if original had one
121
+ item += "\n" if element.end_with?("\n") && !item.end_with?("\n")
122
+ @truncated = true
123
+ end
124
+
125
+ # --- total chars check ---
126
+ if @max_chars && item.is_a?(String)
127
+ remaining = @max_chars - @total_chars
128
+ if remaining <= 0
129
+ @chars_full = true
130
+ @truncated = true
131
+ return
132
+ end
133
+ if item.length > remaining
134
+ # If original line ends with \n we must preserve it, so reserve 1
135
+ # byte for it — this keeps total_chars strictly within max_chars.
136
+ needs_newline = element.is_a?(String) && element.end_with?("\n")
137
+ cut = needs_newline ? [remaining - 1, 0].max : remaining
138
+ item = item[0, cut]
139
+ item += "\n" if needs_newline && !item.end_with?("\n")
140
+ @chars_full = true
141
+ @truncated = true
142
+ end
143
+ end
144
+
145
+ @items << item
146
+ @total_chars += item.length if item.is_a?(String)
147
+
148
+ trim_if_needed
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "time"
5
+
6
+ module Octo
7
+ # Thread-safe daily-rotating file logger.
8
+ #
9
+ # Log files are written to ~/.octo/logger/octo-YYYY-MM-DD.log.
10
+ # At most 7 daily log files are kept; older ones are pruned automatically.
11
+ #
12
+ # Usage (anywhere in the codebase):
13
+ # Octo::Logger.info("server started")
14
+ # Octo::Logger.debug("tool result", tool: "shell", exit_code: 0)
15
+ # Octo::Logger.warn("retry attempt", n: 3)
16
+ # Octo::Logger.error("unhandled exception", error: e)
17
+ module Logger
18
+ LOG_DIR = File.join(Dir.home, ".octo", "logger").freeze
19
+ MAX_LOG_FILES = 7
20
+ MUTEX = Mutex.new
21
+
22
+ # Level constants (numeric, for future filtering)
23
+ LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }.freeze
24
+
25
+ # Minimum level to echo to $stderr when console output is enabled.
26
+ # :debug → all; :info → info/warn/error; :warn → warn/error only
27
+ CONSOLE_MIN_LEVEL = :info
28
+
29
+ class << self
30
+ # Enable/disable echoing log lines to $stderr (in addition to the file).
31
+ # Call Octo::Logger.console = true from server startup to activate.
32
+ attr_writer :console
33
+
34
+ private def console?
35
+ @console ||= false
36
+ end
37
+
38
+ # Path of the log file currently being written to (today's file).
39
+ # File may not exist yet if no log has been emitted today — callers
40
+ # should check File.exist? before reading.
41
+ def current_log_file
42
+ log_file_path(Time.now)
43
+ end
44
+
45
+ # Log at DEBUG level.
46
+ def debug(message, **context)
47
+ write_log(:debug, message, context)
48
+ end
49
+
50
+ # Log at INFO level.
51
+ def info(message, **context)
52
+ write_log(:info, message, context)
53
+ end
54
+
55
+ # Log at WARN level.
56
+ def warn(message, **context)
57
+ write_log(:warn, message, context)
58
+ end
59
+
60
+ # Log at ERROR level. Accepts an optional :error key that may be an
61
+ # Exception; its backtrace is appended automatically.
62
+ def error(message, **context)
63
+ write_log(:error, message, context)
64
+ end
65
+
66
+ private def write_log(level, message, context = {})
67
+ now = Time.now
68
+ line = format_line(now, level, message, context)
69
+
70
+ MUTEX.synchronize do
71
+ ensure_log_dir
72
+ File.open(log_file_path(now), "a") { |f| f.puts(line) }
73
+ prune_old_logs
74
+ echo_to_console(level, line) if console?
75
+ end
76
+ rescue StandardError
77
+ # Never let logger errors crash the main process.
78
+ nil
79
+ end
80
+
81
+ private def echo_to_console(level, line)
82
+ return if LEVELS[level] < LEVELS[CONSOLE_MIN_LEVEL]
83
+ $stderr.puts(line)
84
+ rescue StandardError
85
+ nil
86
+ end
87
+
88
+ private def format_line(time, level, message, context)
89
+ timestamp = time.strftime("%Y-%m-%dT%H:%M:%S.%3N%z")
90
+ tag = level.to_s.upcase.ljust(5)
91
+ base = "[#{timestamp}] #{tag} #{message}"
92
+
93
+ if context.empty?
94
+ base
95
+ else
96
+ # Expand exception objects for :error key
97
+ if (err = context[:error]).is_a?(Exception)
98
+ context = context.merge(
99
+ error: "#{err.class}: #{err.message}",
100
+ backtrace: (err.backtrace || []).first(10).join(" | ")
101
+ )
102
+ end
103
+ pairs = context.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
104
+ "#{base} | #{pairs}"
105
+ end
106
+ end
107
+
108
+ private def log_file_path(time)
109
+ File.join(LOG_DIR, "octo-#{time.strftime('%Y-%m-%d')}.log")
110
+ end
111
+
112
+ private def ensure_log_dir
113
+ FileUtils.mkdir_p(LOG_DIR) unless Dir.exist?(LOG_DIR)
114
+ end
115
+
116
+ # Remove log files older than MAX_LOG_FILES days.
117
+ private def prune_old_logs
118
+ logs = Dir.glob(File.join(LOG_DIR, "octo-*.log")).sort
119
+ excess = logs.length - MAX_LOG_FILES
120
+ logs.first(excess).each { |f| File.delete(f) } if excess > 0
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ module Utils
5
+ # Spawn child processes in an environment that has the user's shell rc
6
+ # files sourced — so version managers (mise / rbenv / asdf / nvm) and
7
+ # custom PATH entries are active, even when the octo server itself
8
+ # was started by launchd / a desktop icon with a minimal PATH.
9
+ #
10
+ # ## Approach: manual `source` + `exec`
11
+ #
12
+ # Instead of using `$SHELL -l -i -c` (which prints rc banners, triggers
13
+ # job-control warnings in non-tty contexts, and may not even work as
14
+ # expected under launchd), we build an inline shell snippet:
15
+ #
16
+ # { source ~/.zshenv; source ~/.zprofile; source ~/.zshrc; } 1>&2
17
+ # exec <target-cmd>
18
+ #
19
+ # Then invoke it with plain `zsh -c <snippet>` (NO -l / -i flags).
20
+ #
21
+ # Why this wins:
22
+ #
23
+ # - `source ~/.zshrc` runs user's rc code including `eval "$(mise activate zsh)"`
24
+ # which injects the correct PATH (so `node`/`ruby`/`gem` resolve).
25
+ # - `{ … } 1>&2` redirects ALL rc-time output (banners, welcome msgs,
26
+ # mise warnings) to stderr, keeping target's stdout CLEAN — critical
27
+ # for JSON-RPC stdio channels like chrome-devtools-mcp.
28
+ # - `exec` replaces the shell with the target process, so our pipe's
29
+ # child is the target itself (pid / signals / waitpid all work).
30
+ # - No `-i`, so no "no job control in this shell" warnings.
31
+ # - No `-l` needed because we explicitly source what we need.
32
+ #
33
+ # ## Method: login_shell_command
34
+ #
35
+ # Build argv for `Open3.popen3` / `Process.spawn` that runs `command`
36
+ # with rc files pre-sourced. Returns argv, not a running process —
37
+ # caller picks the right Open3 method for their needs.
38
+ module LoginShell
39
+ # Build argv that runs `command` inside a shell with rc files sourced.
40
+ #
41
+ # @param command [String] shell-ready command (caller quotes user input).
42
+ # @return [Array<String>] argv for Open3.popen3 / Process.spawn.
43
+ def self.login_shell_command(command)
44
+ shell = ENV["SHELL"].to_s
45
+ shell = "/bin/bash" if shell.empty? || !File.executable?(shell)
46
+ name = File.basename(shell)
47
+ name = "bash" unless %w[zsh bash].include?(name)
48
+ shell = "/bin/bash" if name == "bash" && !File.executable?(shell)
49
+
50
+ rc_sources = rc_source_snippet(name)
51
+
52
+ # { rc_sources; } 1>&2 — send rc-time stdout to stderr so target's
53
+ # stdout is pristine. `exec` replaces the shell with target.
54
+ script = "{ #{rc_sources}; } 1>&2; exec #{command}"
55
+ [shell, "-c", script]
56
+ end
57
+
58
+ # Per-shell rc source chain. Order matters:
59
+ # zsh: .zshenv → .zprofile → .zshrc (login + interactive equivalent)
60
+ # bash: .profile → .bash_profile → .bashrc
61
+ def self.rc_source_snippet(shell_name)
62
+ files =
63
+ case shell_name
64
+ when "zsh" then %w[.zshenv .zprofile .zshrc]
65
+ else %w[.profile .bash_profile .bashrc]
66
+ end
67
+
68
+ files.map { |f| %([ -f "$HOME/#{f}" ] && . "$HOME/#{f}") }.join("; ")
69
+ end
70
+ end
71
+ end
72
+ end