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,191 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # dingtalk_setup.rb — DingTalk channel setup via Device Flow (QR scan).
5
+ #
6
+ # Modes:
7
+ # --print-qr Phase 1+2: call init/begin, print QR URL as JSON, exit immediately.
8
+ # --poll <device_code> Phase 3+4+5: poll until SUCCESS, save credentials, wait for WS.
9
+ #
10
+ # Environment:
11
+ # OCTO_SERVER_PORT, OCTO_SERVER_HOST — octo server coordinates
12
+
13
+ require "json"
14
+ require "net/http"
15
+ require "net/https"
16
+ require "uri"
17
+
18
+ DINGTALK_REG_BASE = "https://oapi.dingtalk.com"
19
+ # Registration source ID assigned by DingTalk (not a brand string — do not rebrand).
20
+ DINGTALK_REG_SOURCE = "DING_DWS_CLAW"
21
+ POLL_INTERVAL = 3
22
+ POLL_TIMEOUT = 300
23
+
24
+ OCTO_SERVER_URL = begin
25
+ url = "http://#{ENV.fetch("OCTO_SERVER_HOST")}:#{ENV.fetch("OCTO_SERVER_PORT")}"
26
+ uri = URI.parse(url)
27
+ raise "Invalid OCTO_SERVER_URL: #{url}" unless uri.is_a?(URI::HTTP) && uri.host && uri.port
28
+ url
29
+ end
30
+
31
+ def step(msg); puts("[dingtalk-setup] #{msg}"); end
32
+ def ok(msg); puts("[dingtalk-setup] ✅ #{msg}"); end
33
+ def warn(msg); puts("[dingtalk-setup] ⚠️ #{msg}"); end
34
+ def fail!(msg)
35
+ puts("[dingtalk-setup] ❌ #{msg}")
36
+ exit 1
37
+ end
38
+
39
+ def post_json(url, payload)
40
+ uri = URI.parse(url)
41
+ http = Net::HTTP.new(uri.host, uri.port)
42
+ http.use_ssl = uri.scheme == "https"
43
+ req = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
44
+ req.body = JSON.generate(payload)
45
+ resp = http.request(req)
46
+ data = JSON.parse(resp.body)
47
+ fail! "API error (#{resp.code}): #{data["errmsg"] || resp.body}" if data["errcode"] && data["errcode"] != 0
48
+ data
49
+ rescue JSON::ParserError => e
50
+ fail! "JSON parse error from #{url}: #{e.message}"
51
+ end
52
+
53
+ def server_post(path, body)
54
+ uri = URI(OCTO_SERVER_URL)
55
+ Net::HTTP.start(uri.host, uri.port, open_timeout: 3, read_timeout: 10) do |h|
56
+ req = Net::HTTP::Post.new(path, "Content-Type" => "application/json")
57
+ req.body = JSON.generate(body)
58
+ h.request(req)
59
+ end
60
+ end
61
+
62
+ def server_get(path)
63
+ uri = URI(OCTO_SERVER_URL)
64
+ Net::HTTP.start(uri.host, uri.port, open_timeout: 3, read_timeout: 10) do |h|
65
+ h.request(Net::HTTP::Get.new(path))
66
+ end
67
+ end
68
+
69
+ # ── Mode: --print-qr ─────────────────────────────────────────────────────────
70
+ # Call init + begin, print JSON with qr_url / device_code / expires_in, exit 0.
71
+ def mode_print_qr
72
+ step "Phase 1 — Starting DingTalk Device Flow registration..."
73
+
74
+ init_data = post_json("#{DINGTALK_REG_BASE}/app/registration/init",
75
+ { source: DINGTALK_REG_SOURCE })
76
+ nonce = init_data["nonce"].to_s.strip
77
+ fail! "Missing nonce in init response" if nonce.empty?
78
+
79
+ begin_data = post_json("#{DINGTALK_REG_BASE}/app/registration/begin", { nonce: nonce })
80
+ device_code = begin_data["device_code"].to_s.strip
81
+ qr_url = begin_data["verification_uri_complete"].to_s.strip
82
+ expires_in = (begin_data["expires_in"] || POLL_TIMEOUT).to_i
83
+
84
+ fail! "Missing device_code in begin response" if device_code.empty?
85
+ fail! "Missing verification_uri_complete" if qr_url.empty?
86
+
87
+ ok "Device Flow started. QR expires in #{expires_in}s."
88
+ puts JSON.generate({ qr_url: qr_url, device_code: device_code, expires_in: expires_in })
89
+ end
90
+
91
+ # ── Mode: --poll <device_code> ────────────────────────────────────────────────
92
+ # Poll until SUCCESS or a terminal state. Exits with:
93
+ # 0 — SUCCESS: credentials saved and adapter started
94
+ # 2 — WAITING: user hasn't scanned yet (Agent should ask user to scan and retry)
95
+ # 1 — terminal failure (expired, fail, or server error)
96
+ def mode_poll(device_code, expires_in: POLL_TIMEOUT, interval: POLL_INTERVAL)
97
+ step "Phase 3 — Checking DingTalk authorization..."
98
+
99
+ client_id = nil
100
+ client_secret = nil
101
+ deadline = Time.now + expires_in
102
+
103
+ loop do
104
+ if Time.now > deadline
105
+ puts "[dingtalk-setup] WAITING_TIMEOUT"
106
+ exit 2
107
+ end
108
+
109
+ poll_data = post_json("#{DINGTALK_REG_BASE}/app/registration/poll",
110
+ { device_code: device_code })
111
+ status = poll_data["status"].to_s.upcase
112
+
113
+ case status
114
+ when "WAITING"
115
+ puts "[dingtalk-setup] WAITING"
116
+ exit 2
117
+ when "SUCCESS"
118
+ client_id = poll_data["client_id"].to_s.strip
119
+ client_secret = poll_data["client_secret"].to_s.strip
120
+ fail! "Authorization succeeded but missing client credentials" if client_id.empty? || client_secret.empty?
121
+ ok "Authorization complete! client_id=#{client_id}"
122
+ break
123
+ when "EXPIRED"
124
+ fail! "Authorization QR code expired. Please re-run."
125
+ when "FAIL"
126
+ fail! "Authorization failed: #{poll_data["fail_reason"] || "unknown reason"}"
127
+ else
128
+ warn "Unknown status=#{status}, retrying..."
129
+ sleep interval
130
+ end
131
+ end
132
+
133
+ # ── Phase 4: Save credentials to octo server ─────────────────────────────
134
+ step "Phase 4 — Saving credentials to octo server..."
135
+
136
+ begin
137
+ res = server_post("/api/channels/dingtalk",
138
+ { client_id: client_id, client_secret: client_secret, enabled: true })
139
+ if res.code.to_i == 200
140
+ ok "Credentials saved, DingTalk Stream adapter starting..."
141
+ else
142
+ body = JSON.parse(res.body) rescue { "error" => res.body }
143
+ fail! "Server rejected credentials: #{body["error"] || res.body}"
144
+ end
145
+ rescue StandardError => e
146
+ fail! "Could not reach octo server: #{e.message}"
147
+ end
148
+
149
+ # ── Phase 5: Wait for Stream Mode WebSocket to connect ─────────────────────
150
+ step "Phase 5 — Waiting for DingTalk Stream connection..."
151
+
152
+ ws_ready = false
153
+ ws_deadline = Time.now + 30
154
+
155
+ loop do
156
+ break if Time.now > ws_deadline
157
+ begin
158
+ res = server_get("/api/channels")
159
+ channels = JSON.parse(res.body)["channels"] || []
160
+ dingtalk = channels.find { |c| c["platform"] == "dingtalk" }
161
+ if dingtalk&.fetch("running", false)
162
+ ws_ready = true
163
+ break
164
+ end
165
+ rescue StandardError => e
166
+ warn "Channel status check failed: #{e.message}"
167
+ end
168
+ sleep 2
169
+ end
170
+
171
+ if ws_ready
172
+ ok "DingTalk Stream WebSocket connected."
173
+ else
174
+ warn "Stream connection not confirmed within 30s — it may still be starting."
175
+ end
176
+
177
+ ok "🎉 DingTalk channel setup complete! Search for your robot in DingTalk to start chatting."
178
+ ok " client_id: #{client_id}"
179
+ end
180
+
181
+ # ── Entry point ───────────────────────────────────────────────────────────────
182
+ case ARGV[0]
183
+ when "--print-qr"
184
+ mode_print_qr
185
+ when "--poll"
186
+ device_code = ARGV[1].to_s.strip
187
+ fail! "Usage: dingtalk_setup.rb --poll <device_code>" if device_code.empty?
188
+ mode_poll(device_code)
189
+ else
190
+ fail! "Usage: dingtalk_setup.rb --print-qr | --poll <device_code>"
191
+ end
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # discord_setup.rb — Discord channel setup helper.
5
+ #
6
+ # Discord's developer portal requires manual interaction (hCaptcha + private API), so the
7
+ # Agent uses the browser only as a container — it navigates to the portal and the user
8
+ # creates the App manually, then pastes back the bot token and application id. This
9
+ # script handles everything a shell can do: emit the portal URL, validate the token
10
+ # against /users/@me, save to the octo server, generate the OAuth2 invite URL, and
11
+ # poll until the bot is in at least one guild.
12
+ #
13
+ # Modes:
14
+ # --portal-url Print the Discord developer portal URL (stdout, single line)
15
+ # --validate <token> Validate bot_token via /users/@me, then POST to server
16
+ # --invite-url <client_id> Print the OAuth2 invite URL (stdout, single line)
17
+ # --watch-guild Long-poll /users/@me/guilds via the saved token
18
+ # until at least one guild appears (or timeout)
19
+ # --bot-info <token> Print {id, username} JSON for an unsaved token
20
+ #
21
+ # Environment:
22
+ # OCTO_SERVER_HOST default 127.0.0.1
23
+ # OCTO_SERVER_PORT default 8888
24
+
25
+ require "json"
26
+ require "net/http"
27
+ require "net/https"
28
+ require "uri"
29
+ require "openssl"
30
+ require "cgi"
31
+ require "yaml"
32
+
33
+ DISCORD_API_BASE = "https://discord.com/api/v10"
34
+ DISCORD_OAUTH_BASE = "https://discord.com/oauth2/authorize"
35
+ DISCORD_PORTAL_URL = "https://discord.com/developers/applications"
36
+ DEFAULT_BOT_PERMS = "274877990912"
37
+ DEFAULT_BOT_SCOPES = "bot applications.commands"
38
+ WATCH_GUILD_DEADLINE = 10 * 60
39
+ WATCH_GUILD_INTERVAL = 3
40
+ USER_AGENT = "DiscordBot (https://github.com/octoai/octo, 1.0)"
41
+
42
+ OCTO_SERVER_URL = begin
43
+ host = ENV.fetch("OCTO_SERVER_HOST", "127.0.0.1")
44
+ port = ENV.fetch("OCTO_SERVER_PORT", "8888")
45
+ "http://#{host}:#{port}"
46
+ end
47
+
48
+ def step(msg); $stderr.puts("[discord-setup] #{msg}"); end
49
+ def ok(msg); $stderr.puts("[discord-setup] #{msg}"); end
50
+ def warn!(msg); $stderr.puts("[discord-setup] #{msg}"); end
51
+
52
+ def fail!(msg, json: false)
53
+ if json
54
+ $stdout.puts(JSON.generate({ error: msg }))
55
+ else
56
+ $stderr.puts("[discord-setup] #{msg}")
57
+ end
58
+ exit 1
59
+ end
60
+
61
+ def discord_get(path, bot_token:, timeout: 15)
62
+ uri = URI("#{DISCORD_API_BASE}#{path}")
63
+ http = Net::HTTP.new(uri.host, uri.port)
64
+ http.use_ssl = true
65
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
66
+ http.read_timeout = timeout
67
+ http.open_timeout = 10
68
+
69
+ req = Net::HTTP::Get.new(uri.request_uri)
70
+ req["Authorization"] = "Bot #{bot_token}"
71
+ req["User-Agent"] = USER_AGENT
72
+ req["Accept"] = "application/json"
73
+
74
+ res = http.request(req)
75
+ body = res.body.to_s
76
+ parsed = (JSON.parse(body) rescue nil)
77
+
78
+ unless res.is_a?(Net::HTTPSuccess)
79
+ msg = parsed.is_a?(Hash) ? (parsed["message"] || body.slice(0, 200)) : body.slice(0, 200)
80
+ raise "Discord HTTP #{res.code} #{path}: #{msg}"
81
+ end
82
+ parsed
83
+ end
84
+
85
+ def saved_bot_token
86
+ yml_path = File.expand_path("~/.octo/channels.yml")
87
+ return nil unless File.exist?(yml_path)
88
+ data = YAML.safe_load_file(yml_path, permitted_classes: [Symbol], aliases: true) rescue nil
89
+ data&.dig("channels", "discord", "bot_token") || data&.dig(:channels, :discord, :bot_token)
90
+ end
91
+
92
+ def save_to_server(bot_token:)
93
+ uri = URI("#{OCTO_SERVER_URL}/api/channels/discord")
94
+ body = JSON.generate({ bot_token: bot_token })
95
+
96
+ http = Net::HTTP.new(uri.host, uri.port)
97
+ http.read_timeout = 30
98
+ http.open_timeout = 5
99
+
100
+ req = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
101
+ req.body = body
102
+
103
+ res = http.request(req)
104
+ data = JSON.parse(res.body) rescue {}
105
+
106
+ unless res.is_a?(Net::HTTPSuccess) && data["ok"]
107
+ fail!("Failed to save Discord config: #{data["error"] || res.body.slice(0, 200)}")
108
+ end
109
+ end
110
+
111
+ mode_idx = ARGV.index { |a| a.start_with?("--") }
112
+ mode = mode_idx ? ARGV[mode_idx] : nil
113
+ arg = mode_idx ? ARGV[mode_idx + 1] : nil
114
+
115
+ case mode
116
+ when "--portal-url"
117
+ $stdout.puts(DISCORD_PORTAL_URL)
118
+ exit 0
119
+
120
+ when "--validate"
121
+ fail!("--validate requires <bot_token>") if arg.to_s.strip.empty?
122
+ bot_token = arg.strip
123
+ step("Validating bot token against Discord API...")
124
+ begin
125
+ me = discord_get("/users/@me", bot_token: bot_token)
126
+ rescue => e
127
+ fail!("Token validation failed: #{e.message}")
128
+ end
129
+
130
+ bot_id = me["id"].to_s
131
+ username = me["username"].to_s
132
+ fail!("Empty bot id from /users/@me") if bot_id.empty?
133
+
134
+ ok("Authenticated as #{username} (id=#{bot_id})")
135
+ step("Saving credentials via octo server...")
136
+ save_to_server(bot_token: bot_token)
137
+ ok("Discord channel configured")
138
+
139
+ $stdout.puts(JSON.generate({ bot_id: bot_id, username: username }))
140
+ exit 0
141
+
142
+ when "--bot-info"
143
+ fail!("--bot-info requires <bot_token>", json: true) if arg.to_s.strip.empty?
144
+ begin
145
+ me = discord_get("/users/@me", bot_token: arg.strip)
146
+ rescue => e
147
+ fail!(e.message, json: true)
148
+ end
149
+ $stdout.puts(JSON.generate({ bot_id: me["id"], username: me["username"] }))
150
+ exit 0
151
+
152
+ when "--invite-url"
153
+ fail!("--invite-url requires <client_id>") if arg.to_s.strip.empty?
154
+ client_id = arg.strip
155
+ url = "#{DISCORD_OAUTH_BASE}?client_id=#{CGI.escape(client_id)}" \
156
+ "&permissions=#{DEFAULT_BOT_PERMS}" \
157
+ "&scope=#{CGI.escape(DEFAULT_BOT_SCOPES)}"
158
+ $stdout.puts(url)
159
+ exit 0
160
+
161
+ when "--watch-guild"
162
+ bot_token = saved_bot_token
163
+ fail!("No saved bot_token in ~/.octo/channels.yml — run --validate first") if bot_token.to_s.empty?
164
+
165
+ step("Waiting for the bot to be added to a guild (timeout: #{WATCH_GUILD_DEADLINE / 60} min)...")
166
+ deadline = Time.now + WATCH_GUILD_DEADLINE
167
+
168
+ loop do
169
+ fail!("Timed out waiting for the bot to join a guild. Open the invite URL again to retry.") if Time.now > deadline
170
+
171
+ begin
172
+ guilds = discord_get("/users/@me/guilds", bot_token: bot_token)
173
+ rescue => e
174
+ warn!("Poll error (will retry): #{e.message}")
175
+ sleep WATCH_GUILD_INTERVAL
176
+ next
177
+ end
178
+
179
+ if guilds.is_a?(Array) && !guilds.empty?
180
+ g = guilds.first
181
+ ok("Bot added to guild: #{g["name"]} (id=#{g["id"]})")
182
+ $stdout.puts(JSON.generate({ guild_id: g["id"], guild_name: g["name"], total: guilds.length }))
183
+ exit 0
184
+ end
185
+
186
+ sleep WATCH_GUILD_INTERVAL
187
+ end
188
+
189
+ else
190
+ $stderr.puts(<<~USAGE)
191
+ Usage:
192
+ ruby discord_setup.rb --portal-url
193
+ ruby discord_setup.rb --validate <bot_token>
194
+ ruby discord_setup.rb --bot-info <bot_token>
195
+ ruby discord_setup.rb --invite-url <client_id>
196
+ ruby discord_setup.rb --watch-guild
197
+ USAGE
198
+ exit 1
199
+ end