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,413 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'pathname'
6
+
7
+ # Import skills from external AI tool installations into ~/.octo/skills/.
8
+ #
9
+ # Supported sources:
10
+ # - OpenClaw: ~/.openclaw/skills/, ~/.openclaw/workspace/skills/,
11
+ # ~/.openclaw/workspace/.agents/skills/
12
+ #
13
+ # Each source is imported into a dedicated category subdirectory under ~/.octo/skills/,
14
+ # e.g. ~/.octo/skills/openclaw-imports/<skill-name>/. This keeps imported skills
15
+ # isolated from the user's own skills and makes the origin traceable.
16
+ #
17
+ # Usage: ruby import_external_skills.rb [--source <name>] [--dry-run] [--yes]
18
+ #
19
+ # Options:
20
+ # --source <name> Import only from the named source (e.g. "openclaw").
21
+ # Defaults to all supported sources.
22
+ # --dry-run Preview what would be imported without making any changes.
23
+ # --yes Skip confirmation prompt and execute immediately.
24
+ #
25
+ # Exit codes:
26
+ # 0 - success (including "nothing to import" case)
27
+ # 1 - unexpected error
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Base class for a single-source importer
31
+ # ---------------------------------------------------------------------------
32
+ class ExternalSkillsImporter
33
+ # @param target_skills_dir [Pathname] ~/.octo/skills/
34
+ # @param category_subdir [String] subdirectory name used to group imported skills
35
+ # @param dry_run [Boolean] when true, only preview without making changes
36
+ def initialize(target_skills_dir:, category_subdir:, dry_run: false)
37
+ @target_skills_dir = target_skills_dir
38
+ @target_import_dir = target_skills_dir.join(category_subdir)
39
+ @dry_run = dry_run
40
+ @imported = []
41
+ @errors = []
42
+ end
43
+
44
+ # Run the import for this source.
45
+ # @return [Integer] number of skills imported (or would be imported in dry-run mode)
46
+ def run
47
+ unless source_available?
48
+ puts "[INFO] #{source_label} not found - skipping."
49
+ return 0
50
+ end
51
+
52
+ skills = discover_skills
53
+ if skills.empty?
54
+ puts "[INFO] No #{source_label} skills found - nothing to import."
55
+ return 0
56
+ end
57
+
58
+ skills.each { |skill| process_skill(skill) }
59
+
60
+ @imported.size
61
+ end
62
+
63
+ # Errors encountered during this import run.
64
+ # @return [Array<String>]
65
+ def errors
66
+ @errors.dup
67
+ end
68
+
69
+ # Imported skill records for reporting.
70
+ # @return [Array<Hash>]
71
+ def imported
72
+ @imported.dup
73
+ end
74
+
75
+ # Human-readable name for this source (used in output messages).
76
+ # Subclasses must override.
77
+ # @return [String]
78
+ private def source_label
79
+ raise NotImplementedError
80
+ end
81
+
82
+ # Return true when the source root directory exists on this machine.
83
+ # Subclasses must override.
84
+ # @return [Boolean]
85
+ private def source_available?
86
+ raise NotImplementedError
87
+ end
88
+
89
+ # Discover all valid skill directories from the external source.
90
+ # Each element must be a Hash with at least: { name:, source_dir:, origin: }
91
+ # Subclasses must override.
92
+ # @return [Array<Hash>]
93
+ private def discover_skills
94
+ raise NotImplementedError
95
+ end
96
+
97
+ # Process a single skill: record it for preview, and copy if not in dry-run mode.
98
+ #
99
+ # @param skill [Hash] { name:, source_dir:, origin: }
100
+ private def process_skill(skill)
101
+ name = skill[:name]
102
+ source_dir = Pathname.new(skill[:source_dir])
103
+ dest_dir = @target_import_dir.join(name)
104
+
105
+ action = dest_dir.exist? ? 'updated' : 'imported'
106
+ description = read_description(source_dir.join('SKILL.md'))
107
+
108
+ @imported << {
109
+ name: name,
110
+ action: action,
111
+ description: description,
112
+ dest: dest_dir,
113
+ source_dir: source_dir,
114
+ origin: skill[:origin]
115
+ }
116
+
117
+ return if @dry_run
118
+
119
+ copy_skill(name, source_dir, dest_dir, action)
120
+ rescue StandardError => e
121
+ @errors << "Failed to process '#{name}': #{e.message}"
122
+ end
123
+
124
+ # Copy a single skill directory into @target_import_dir.
125
+ # Existing destinations are removed first so re-running is idempotent.
126
+ #
127
+ # @param name [String]
128
+ # @param source_dir [Pathname]
129
+ # @param dest_dir [Pathname]
130
+ # @param action [String] 'imported' or 'updated'
131
+ private def copy_skill(name, source_dir, dest_dir, action)
132
+ FileUtils.mkdir_p(@target_import_dir)
133
+ FileUtils.rm_rf(dest_dir) if dest_dir.exist?
134
+ FileUtils.mkdir_p(dest_dir)
135
+
136
+ # Copy all contents: SKILL.md, scripts/, assets/, etc.
137
+ source_dir.children.each { |child| FileUtils.cp_r(child, dest_dir) }
138
+ rescue StandardError => e
139
+ @errors << "Failed to import '#{name}': #{e.message}"
140
+ end
141
+
142
+ # Extract the description field from SKILL.md YAML frontmatter.
143
+ # @param skill_file [Pathname]
144
+ # @return [String]
145
+ private def read_description(skill_file)
146
+ return 'No description' unless skill_file.exist?
147
+
148
+ content = skill_file.read
149
+ return $1.strip if content =~ /\A---\s*\n.*?^description:\s*(.+)$/m
150
+
151
+ 'No description'
152
+ rescue StandardError
153
+ 'No description'
154
+ end
155
+ end
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # OpenClaw importer
159
+ # ---------------------------------------------------------------------------
160
+ class OpenClawImporter < ExternalSkillsImporter
161
+ SOURCE_NAME = 'openclaw'
162
+ DEFAULT_OPENCLAW_DIR = File.join(Dir.home, '.openclaw')
163
+
164
+ # @param kwargs forwarded to ExternalSkillsImporter
165
+ def initialize(**kwargs)
166
+ super(category_subdir: 'openclaw-imports', **kwargs)
167
+ @openclaw_dir = Pathname.new(DEFAULT_OPENCLAW_DIR).expand_path
168
+ end
169
+
170
+ private def source_label
171
+ 'OpenClaw (~/.openclaw)'
172
+ end
173
+
174
+ private def source_available?
175
+ openclaw_dirs.any?(&:exist?)
176
+ end
177
+
178
+ # Returns all directories that may contain OpenClaw skills.
179
+ # Each entry is a hash: { root: Pathname, layout: :flat }
180
+ #
181
+ # Mirrors the sources from hermes openclaw_to_hermes.py:
182
+ # - ~/.openclaw/workspace/skills/ (workspace skills)
183
+ # - ~/.openclaw/skills/ (managed/shared skills)
184
+ # - ~/.openclaw/workspace/.agents/skills/ (project-level shared skills)
185
+ #
186
+ # On WSL, also scans the Windows-native %USERPROFILE%\.openclaw directory.
187
+ private def source_dirs
188
+ openclaw_dirs.flat_map do |root|
189
+ [
190
+ root.join('workspace', 'skills'),
191
+ root.join('skills'),
192
+ root.join('workspace', '.agents', 'skills')
193
+ ]
194
+ end.select(&:exist?)
195
+ end
196
+
197
+ # All candidate OpenClaw root directories.
198
+ # On WSL, includes both ~/.openclaw and the Windows-native path.
199
+ private def openclaw_dirs
200
+ dirs = [@openclaw_dir]
201
+ win_home = windows_home
202
+ dirs << win_home.join('.openclaw') if win_home && win_home.join('.openclaw') != @openclaw_dir
203
+ dirs
204
+ end
205
+
206
+ # True when running inside WSL.
207
+ # Mirrors EnvironmentDetector#wsl? — reads /proc/version for "microsoft".
208
+ private def wsl?
209
+ return @wsl if defined?(@wsl)
210
+
211
+ @wsl = File.exist?('/proc/version') &&
212
+ File.read('/proc/version').downcase.include?('microsoft')
213
+ rescue StandardError
214
+ @wsl = false
215
+ end
216
+
217
+ # Resolve the Windows %USERPROFILE% as a WSL-accessible Pathname.
218
+ # Uses powershell.exe (standard in WSL) then wslpath for conversion,
219
+ # mirroring the approach in EnvironmentDetector#wsl_desktop_path.
220
+ # Returns nil when not on WSL or when the path cannot be resolved.
221
+ private def windows_home
222
+ return nil unless wsl?
223
+ return nil if `which powershell.exe 2>/dev/null`.strip.empty?
224
+
225
+ win_path = `powershell.exe -NoProfile -Command '$env:USERPROFILE' 2>/dev/null`.strip.tr("\r\n", '')
226
+ return nil if win_path.empty?
227
+
228
+ linux_path = `wslpath '#{win_path}' 2>/dev/null`.strip
229
+ return nil if linux_path.empty?
230
+
231
+ path = Pathname.new(linux_path)
232
+ path.exist? ? path : nil
233
+ rescue StandardError
234
+ nil
235
+ end
236
+
237
+ private def discover_skills
238
+ skills = []
239
+
240
+ source_dirs.each do |dir|
241
+ dir.children.select(&:directory?).each do |skill_dir|
242
+ next unless skill_dir.join('SKILL.md').exist?
243
+
244
+ skills << {
245
+ name: skill_dir.basename.to_s,
246
+ source_dir: skill_dir,
247
+ origin: dir.basename.to_s
248
+ }
249
+ end
250
+ end
251
+
252
+ skills.sort_by { |s| s[:name] }
253
+ end
254
+ end
255
+
256
+ # ---------------------------------------------------------------------------
257
+ # Coordinator - runs all enabled importers and prints a combined report
258
+ # ---------------------------------------------------------------------------
259
+ class ExternalSkillsImportRunner
260
+ # Register new importer classes here to add support for more sources.
261
+ IMPORTERS = [OpenClawImporter].freeze
262
+ SOURCES = IMPORTERS.map { |klass| klass::SOURCE_NAME }.freeze
263
+
264
+ # @param sources [Array<String>] subset of SOURCES to run; nil means all
265
+ # @param target_skills_dir [String]
266
+ # @param dry_run [Boolean] when true, only preview without making changes
267
+ # @param yes [Boolean] when true, skip confirmation prompt
268
+ def initialize(sources: nil,
269
+ target_skills_dir: File.join(Dir.home, '.octo', 'skills'),
270
+ dry_run: false,
271
+ yes: false)
272
+ @sources = (sources || SOURCES) & SOURCES
273
+ @target_skills_dir = Pathname.new(target_skills_dir).expand_path
274
+ @dry_run = dry_run
275
+ @yes = yes
276
+ end
277
+
278
+ def run
279
+ # In dry-run mode: collect plan and print preview only
280
+ if @dry_run
281
+ importers = build_importers(dry_run: true)
282
+ all_imported = []
283
+ importers.each { |i| i.run; all_imported.concat(i.imported) }
284
+ print_preview(all_imported, dry_run: true)
285
+ return all_imported.size
286
+ end
287
+
288
+ # Normal mode: collect plan first, show preview, then confirm
289
+ preview_importers = build_importers(dry_run: true)
290
+ all_preview = []
291
+ preview_importers.each { |i| i.run; all_preview.concat(i.imported) }
292
+
293
+ if all_preview.empty?
294
+ puts 'Nothing to import.'
295
+ return 0
296
+ end
297
+
298
+ print_preview(all_preview, dry_run: false)
299
+
300
+ unless @yes || confirm?
301
+ puts 'Import cancelled.'
302
+ return 0
303
+ end
304
+
305
+ # Execute the actual import
306
+ importers = build_importers(dry_run: false)
307
+ all_imported = []
308
+ all_errors = []
309
+
310
+ importers.each do |importer|
311
+ importer.run
312
+ all_imported.concat(importer.imported)
313
+ all_errors.concat(importer.errors)
314
+ end
315
+
316
+ print_summary(all_imported, all_errors)
317
+ all_imported.size
318
+ end
319
+
320
+ private def build_importers(dry_run:)
321
+ common = { target_skills_dir: @target_skills_dir, dry_run: dry_run }
322
+
323
+ IMPORTERS
324
+ .select { |klass| @sources.include?(klass::SOURCE_NAME) }
325
+ .map { |klass| klass.new(**common) }
326
+ end
327
+
328
+ # Print a Hermes-style preview of what would be / will be imported.
329
+ # @param skills [Array<Hash>]
330
+ # @param dry_run [Boolean]
331
+ private def print_preview(skills, dry_run:)
332
+ if dry_run
333
+ puts 'Dry Run Results'
334
+ puts ' No files will be modified. This is a preview of what would happen.'
335
+ else
336
+ puts 'Import Preview'
337
+ puts ' The following skills will be imported/updated:'
338
+ end
339
+ puts
340
+
341
+ if skills.empty?
342
+ puts ' (nothing to import)'
343
+ else
344
+ label_width = skills.map { |s| s[:origin].length }.max || 0
345
+ skills.each do |s|
346
+ action_marker = s[:action] == 'updated' ? '~' : '✓'
347
+ puts " #{action_marker} Would import: #{s[:origin].ljust(label_width)} → #{s[:dest]}"
348
+ end
349
+ end
350
+
351
+ puts
352
+ puts " Summary: #{skills.size} skill(s) would be #{dry_run ? 'imported' : 'imported/updated'}"
353
+ puts
354
+ end
355
+
356
+ # Print summary after actual import.
357
+ private def print_summary(imported, errors)
358
+ puts '=' * 60
359
+
360
+ if imported.empty? && errors.empty?
361
+ puts 'Nothing was imported.'
362
+ elsif imported.any?
363
+ puts "Import complete! #{imported.size} skill(s) ready:\n\n"
364
+ imported.each do |s|
365
+ action_label = s[:action] == 'updated' ? '[updated]' : '[new]'
366
+ puts " #{action_label} #{s[:name]}"
367
+ puts " #{s[:description]}"
368
+ puts " -> #{s[:dest]}"
369
+ puts
370
+ end
371
+ puts 'Skills will be available automatically next time Octo starts.'
372
+ end
373
+
374
+ if errors.any?
375
+ puts 'Errors:'
376
+ errors.each { |e| puts " - #{e}" }
377
+ end
378
+
379
+ puts '=' * 60
380
+ end
381
+
382
+ # Prompt user for confirmation.
383
+ # @return [Boolean]
384
+ private def confirm?
385
+ print 'Proceed with import? [y/N] '
386
+ $stdout.flush
387
+ answer = $stdin.gets&.strip&.downcase
388
+ answer == 'y' || answer == 'yes'
389
+ end
390
+ end
391
+
392
+ # -- Entry point ------------------------------------------------------------
393
+ if __FILE__ == $PROGRAM_NAME
394
+ require 'optparse'
395
+
396
+ options = {}
397
+
398
+ OptionParser.new do |opts|
399
+ opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options]"
400
+ opts.on('--source NAME',
401
+ "Import only from NAME (e.g. openclaw). Supported: #{ExternalSkillsImportRunner::SOURCES.join(', ')}") do |name|
402
+ options[:sources] = [name]
403
+ end
404
+ opts.on('--dry-run', 'Preview what would be imported without making any changes.') do
405
+ options[:dry_run] = true
406
+ end
407
+ opts.on('--yes', '-y', 'Skip confirmation prompt and execute immediately.') do
408
+ options[:yes] = true
409
+ end
410
+ end.parse!
411
+
412
+ ExternalSkillsImportRunner.new(**options).run
413
+ end
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'uri'
5
+ require 'net/http'
6
+ require 'json'
7
+
8
+ require_relative '../../skill-add/scripts/install_from_zip'
9
+
10
+ class BuiltinSkillsInstaller
11
+ PRIMARY_HOST = ENV.fetch('OCTO_LICENSE_SERVER', 'https://www.octo.com')
12
+ FALLBACK_HOST = 'https://octo.up.railway.app'
13
+ API_HOSTS = ENV['OCTO_LICENSE_SERVER'] ? [PRIMARY_HOST] : [PRIMARY_HOST, FALLBACK_HOST]
14
+ API_PATH = '/api/v1/skills/builtin'
15
+ API_OPEN_TIMEOUT = 5
16
+ API_READ_TIMEOUT = 10
17
+
18
+ def initialize
19
+ @target_dir = File.join(Dir.home, '.octo', 'skills')
20
+ @installed = 0
21
+ @skipped_existing = 0
22
+ @attempted = 0
23
+ @errors = []
24
+ end
25
+
26
+ def run
27
+ skills = fetch_skill_list
28
+ if skills.nil? || skills.empty?
29
+ emit_summary
30
+ return
31
+ end
32
+
33
+ skills.each { |skill| install_one(skill) }
34
+ ensure
35
+ emit_summary
36
+ end
37
+
38
+ private def fetch_skill_list
39
+ API_HOSTS.each do |host|
40
+ begin
41
+ uri = URI.parse(host + API_PATH)
42
+ Net::HTTP.start(uri.host, uri.port,
43
+ use_ssl: uri.scheme == 'https',
44
+ open_timeout: API_OPEN_TIMEOUT,
45
+ read_timeout: API_READ_TIMEOUT) do |http|
46
+ response = http.request(Net::HTTP::Get.new(uri.request_uri))
47
+ if response.code.to_i == 200
48
+ payload = JSON.parse(response.body)
49
+ return Array(payload['skills'])
50
+ else
51
+ @errors << "API #{host}: HTTP #{response.code}"
52
+ end
53
+ end
54
+ rescue StandardError => e
55
+ @errors << "API #{host}: #{e.class}: #{e.message}"
56
+ end
57
+ end
58
+ nil
59
+ end
60
+
61
+ private def install_one(skill)
62
+ name = skill['name'].to_s
63
+ download_url = skill['download_url'].to_s
64
+ @attempted += 1
65
+
66
+ if name.empty? || download_url.empty?
67
+ @errors << "skill payload missing name or download_url: #{skill.inspect}"
68
+ return
69
+ end
70
+
71
+ result = ZipSkillInstaller.new(
72
+ download_url,
73
+ skill_name: name,
74
+ target_dir: @target_dir,
75
+ skip_if_exists: true
76
+ ).perform
77
+ @installed += result[:installed].size
78
+ @skipped_existing += result[:skipped].size
79
+ @errors.concat(result[:errors]) if result[:errors].any?
80
+ rescue StandardError => e
81
+ @errors << "#{name}: #{e.class}: #{e.message}"
82
+ end
83
+
84
+ private def emit_summary
85
+ unless @errors.empty?
86
+ warn '[install_builtin_skills] non-fatal errors:'
87
+ @errors.each { |e| warn " - #{e}" }
88
+ end
89
+ puts JSON.generate(
90
+ installed: @installed,
91
+ attempted: @attempted,
92
+ skipped_existing: @skipped_existing
93
+ )
94
+ end
95
+ end
96
+
97
+ BuiltinSkillsInstaller.new.run if __FILE__ == $0
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: persist-memory
3
+ description: Persist information to long-term memory at ~/.octo/memories/. Use when the user asks you to remember/note something, or when reviewing a finished conversation for facts worth keeping. Handles file naming, topic merging, frontmatter, and size limits.
4
+ fork_agent: true
5
+ user-invocable: false
6
+ auto_summarize: true
7
+ forbidden_tools:
8
+ - web_search
9
+ - web_fetch
10
+ - browser
11
+ ---
12
+
13
+ # Persist Memory Subagent
14
+
15
+ You are a **Memory Persistence Subagent** — a pure executor. The caller has already decided that something must be written. Your job is to write it correctly: pick the right file, merge with existing content, respect the size limit.
16
+
17
+ You do NOT decide whether to write. If the task description tells you to persist X, you persist X.
18
+
19
+ ## Existing Memory Files
20
+
21
+ The following memory files are pre-loaded for you — **do NOT re-scan the directory** with `terminal` or `file_reader`.
22
+
23
+ <%= memories_meta %>
24
+
25
+ Each file uses YAML frontmatter:
26
+
27
+ ```
28
+ ---
29
+ topic: <topic name>
30
+ description: <one-line description>
31
+ ---
32
+ <content in concise Markdown>
33
+ ```
34
+
35
+ ## Workflow
36
+
37
+ For each item to persist:
38
+
39
+ ### Step 1: Pick a target file
40
+
41
+ Scan the list above:
42
+
43
+ - **Matching topic exists** → read it with `file_reader(path: "~/.octo/memories/<filename>")`, integrate the new info, drop stale parts, then `write` the updated version back.
44
+ - **No match** → create a new file at `~/.octo/memories/<topic-slug>.md`.
45
+ - Slug: lowercase, hyphen-separated, descriptive (e.g. `deployment-target.md`, `code-style-preferences.md`).
46
+
47
+ ### Step 2: Write the file
48
+
49
+ Use the `write` tool. Always include the YAML frontmatter shown above.
50
+
51
+ ## Hard constraints (CRITICAL)
52
+
53
+ - Each file MUST stay under **4000 characters of content** (after the frontmatter).
54
+ - If merging would exceed this limit, remove the least important information — do NOT split into multiple files for the same topic.
55
+ - Write concise, factual Markdown — no fluff, no redundant headings.
56
+ - One topic per file. Don't bundle unrelated facts together.
57
+ - Do NOT use `terminal` or `file_reader` to list the memories directory — the list above is authoritative.
58
+
59
+ When done, briefly state what was written (e.g. "Updated deployment-target.md") or `No memory updates needed.` if the task description didn't actually require any writes.
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: personal-website
3
+ description: |
4
+ Generate a beautiful personal homepage (linktree-style) and publish it online for the user.
5
+ Reads user info from ~/.octo/agents/USER.md and AI info from ~/.octo/agents/SOUL.md.
6
+ Returns a public URL the user can share.
7
+ Trigger on: "profile card", "homepage", "personal page", "generate my card", "make my card",
8
+ "publish my card", "生成名片", "做名片", "我的名片", "个人主页", "发布主页",
9
+ "delete my card", "删除名片", "删除主页".
10
+ allowed-tools:
11
+ - Bash
12
+ - Read
13
+ - Write
14
+ ---
15
+
16
+ # Profile Homepage Skill
17
+
18
+ Generate a beautiful personal homepage and publish it at a public URL.
19
+
20
+ ---
21
+
22
+ ## Step 1 — Read user info
23
+
24
+ Read `~/.octo/agents/USER.md` and `~/.octo/agents/SOUL.md`.
25
+
26
+ Extract everything you can find:
27
+ - `name` — display name (fallback: "Friend")
28
+ - `occupation` — job title or role (fallback: "")
29
+ - `bio` — short personal description (fallback: "")
30
+ - `links` — **all** social/contact links found, preserve their labels. Common ones to look for:
31
+ GitHub, Twitter/X, LinkedIn, Website, Blog, Email, Instagram, YouTube, Telegram, WeChat, etc.
32
+ Each link: `{ label, url, type }` where type helps pick an icon emoji.
33
+ - `ai_name` — AI assistant name from SOUL.md (fallback: "Octo")
34
+ - `personality` — professional / friendly / creative / concise (from SOUL.md, fallback: "friendly")
35
+
36
+ ---
37
+
38
+ ## Step 2 — Handle delete request
39
+
40
+ If the user asked to **delete** their homepage:
41
+ 1. Find the skill's own directory (same folder as this SKILL.md). Call it `SKILL_DIR`.
42
+ 2. Run:
43
+ ```bash
44
+ ruby SKILL_DIR/publish.rb delete
45
+ ```
46
+ The script reads the slug automatically from `~/octo_workspace/personal_website/token.json`.
47
+ 3. Tell the user their homepage has been removed. Stop here.
48
+
49
+ ---
50
+
51
+ ## Step 3 — Design & generate the HTML
52
+
53
+ Write a **complete, self-contained** HTML file to `/tmp/profile-card.html`.
54
+
55
+ ### You have full creative freedom on:
56
+ - Layout, typography, spacing, color palette
57
+ - Background (solid / gradient / subtle pattern / animated)
58
+ - Link button style (pill / card / underline / ghost / anything)
59
+ - Avatar treatment (large initial letter with color, emoji, geometric shape — no real image needed)
60
+ - Animations (subtle hover effects, entrance fade, etc.)
61
+ - Overall vibe — make it feel like a real personal brand page, not a template
62
+
63
+ ### Hard constraints (must follow):
64
+ - **Single HTML file, zero external resources** — no CDN, no Google Fonts URLs, no `<img src="http...">`.
65
+ Use system fonts: `'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', sans-serif`
66
+ - **Mobile-first, responsive** — `<meta name="viewport">` required, works on phone screens
67
+ - **Valid HTML5**
68
+ - **All links open in `_blank`** with `rel="noopener"`
69
+ - **Badge** somewhere subtle: `made by {ai_name} personal assistant` — small, not intrusive
70
+ - Page `<title>`: `{name}'s Homepage` or similar
71
+
72
+ ### Link icons (use emoji prefix in button text):
73
+ | Type | Emoji |
74
+ |----------|-------|
75
+ | github | 🐙 |
76
+ | twitter/x | 🐦 |
77
+ | linkedin | 💼 |
78
+ | website/blog | 🌐 |
79
+ | email | 📧 |
80
+ | instagram | 📸 |
81
+ | youtube | ▶️ |
82
+ | telegram | ✈️ |
83
+ | default | 🔗 |
84
+
85
+ ---
86
+
87
+ ## Step 4 — Publish
88
+
89
+ Find the skill directory (same folder as this SKILL.md). Call it `SKILL_DIR`.
90
+
91
+ Run:
92
+ ```bash
93
+ ruby SKILL_DIR/publish.rb publish \
94
+ --name "NAME" \
95
+ --html-file /tmp/profile-card.html
96
+ ```
97
+
98
+ - First publish → creates new page, saves token to `~/octo_workspace/personal_website/token.json`
99
+ - Subsequent runs → updates existing page at the same URL
100
+
101
+ Capture stdout. Extract the URL from the output line starting with `✅`.
102
+
103
+ ---
104
+
105
+ ## Step 5 — Done
106
+
107
+ Tell the user their homepage is live. Share the URL. Be warm and natural.
108
+
109
+ Example (adapt tone to personality):
110
+ > Your homepage is live 🌟
111
+ > → http://localhost:3000/~ya-fei
112
+ >
113
+ > It's got all your links in one place. Share it anywhere.