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.
- checksums.yaml +7 -0
- data/.clacky/skills/commit/SKILL.md +423 -0
- data/.clacky/skills/gem-release/SKILL.md +199 -0
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/.clacky/skills/oss-upload/SKILL.md +47 -0
- data/.octorules +106 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +76 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +92 -0
- data/Dockerfile +28 -0
- data/LICENSE.txt +22 -0
- data/POSITIONING.md +46 -0
- data/README.md +134 -0
- data/README_CN.md +134 -0
- data/Rakefile +34 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/bin/octo +7 -0
- data/docs/agent-first-ui-design.md +77 -0
- data/docs/billing-system.md +318 -0
- data/docs/channel-architecture.md +235 -0
- data/docs/engineering-article.md +343 -0
- data/docs/session-skill-invocation.md +69 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/ui2-architecture.md +124 -0
- data/homebrew/README.md +96 -0
- data/homebrew/openocto.rb +24 -0
- data/lib/octo/agent/hook_manager.rb +61 -0
- data/lib/octo/agent/llm_caller.rb +800 -0
- data/lib/octo/agent/memory_updater.rb +246 -0
- data/lib/octo/agent/message_compressor.rb +225 -0
- data/lib/octo/agent/message_compressor_helper.rb +869 -0
- data/lib/octo/agent/next_message_suggester.rb +215 -0
- data/lib/octo/agent/session_serializer.rb +685 -0
- data/lib/octo/agent/skill_auto_creator.rb +114 -0
- data/lib/octo/agent/skill_evolution.rb +61 -0
- data/lib/octo/agent/skill_manager.rb +466 -0
- data/lib/octo/agent/skill_reflector.rb +89 -0
- data/lib/octo/agent/system_prompt_builder.rb +101 -0
- data/lib/octo/agent/time_machine.rb +214 -0
- data/lib/octo/agent/tool_executor.rb +454 -0
- data/lib/octo/agent/tool_registry.rb +150 -0
- data/lib/octo/agent.rb +2180 -0
- data/lib/octo/agent_config.rb +989 -0
- data/lib/octo/agent_profile.rb +112 -0
- data/lib/octo/anthropic_stream_aggregator.rb +137 -0
- data/lib/octo/background_task_registry.rb +324 -0
- data/lib/octo/banner.rb +34 -0
- data/lib/octo/bedrock_stream_aggregator.rb +137 -0
- data/lib/octo/block_font.rb +331 -0
- data/lib/octo/cli.rb +968 -0
- data/lib/octo/client.rb +623 -0
- data/lib/octo/default_agents/SOUL.md +3 -0
- data/lib/octo/default_agents/USER.md +1 -0
- data/lib/octo/default_agents/base_prompt.md +66 -0
- data/lib/octo/default_agents/coding/profile.yml +2 -0
- data/lib/octo/default_agents/coding/system_prompt.md +67 -0
- data/lib/octo/default_agents/general/profile.yml +2 -0
- data/lib/octo/default_agents/general/system_prompt.md +16 -0
- data/lib/octo/default_parsers/doc_parser.rb +69 -0
- data/lib/octo/default_parsers/docx_parser.rb +188 -0
- data/lib/octo/default_parsers/pdf_parser.rb +120 -0
- data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/octo/default_parsers/pptx_parser.rb +140 -0
- data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
- data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
- data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
- data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
- data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
- data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
- data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
- data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
- data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/octo/default_skills/onboard/SKILL.md +578 -0
- data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
- data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
- data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
- data/lib/octo/default_skills/personal-website/publish.rb +235 -0
- data/lib/octo/default_skills/product-help/SKILL.md +123 -0
- data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
- data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
- data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
- data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
- data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
- data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
- data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
- data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
- data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
- data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
- data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
- data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
- data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
- data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
- data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
- data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
- data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
- data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
- data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
- data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
- data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
- data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
- data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/octo/idle_compression_timer.rb +115 -0
- data/lib/octo/json_ui_controller.rb +204 -0
- data/lib/octo/message_format/anthropic.rb +409 -0
- data/lib/octo/message_format/bedrock.rb +361 -0
- data/lib/octo/message_format/open_ai.rb +222 -0
- data/lib/octo/message_history.rb +373 -0
- data/lib/octo/openai_stream_aggregator.rb +130 -0
- data/lib/octo/plain_ui_controller.rb +166 -0
- data/lib/octo/providers.rb +534 -0
- data/lib/octo/server/browser_manager.rb +397 -0
- data/lib/octo/server/channel/adapters/base.rb +82 -0
- data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
- data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
- data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
- data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
- data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
- data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
- data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
- data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
- data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
- data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
- data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
- data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
- data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
- data/lib/octo/server/channel/channel_config.rb +178 -0
- data/lib/octo/server/channel/channel_manager.rb +468 -0
- data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
- data/lib/octo/server/channel.rb +33 -0
- data/lib/octo/server/discover.rb +77 -0
- data/lib/octo/server/epipe_safe_io.rb +105 -0
- data/lib/octo/server/http_server.rb +3554 -0
- data/lib/octo/server/scheduler.rb +317 -0
- data/lib/octo/server/server_master.rb +325 -0
- data/lib/octo/server/session_registry.rb +431 -0
- data/lib/octo/server/web_ui_controller.rb +487 -0
- data/lib/octo/session_manager.rb +385 -0
- data/lib/octo/skill.rb +466 -0
- data/lib/octo/skill_loader.rb +328 -0
- data/lib/octo/tools/base.rb +118 -0
- data/lib/octo/tools/browser.rb +625 -0
- data/lib/octo/tools/edit.rb +165 -0
- data/lib/octo/tools/file_reader.rb +549 -0
- data/lib/octo/tools/glob.rb +162 -0
- data/lib/octo/tools/grep.rb +356 -0
- data/lib/octo/tools/invoke_skill.rb +96 -0
- data/lib/octo/tools/list_tasks.rb +54 -0
- data/lib/octo/tools/redo_task.rb +41 -0
- data/lib/octo/tools/request_user_feedback.rb +84 -0
- data/lib/octo/tools/security.rb +333 -0
- data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
- data/lib/octo/tools/terminal/persistent_session.rb +268 -0
- data/lib/octo/tools/terminal/safe_rm.sh +106 -0
- data/lib/octo/tools/terminal/session_manager.rb +213 -0
- data/lib/octo/tools/terminal.rb +1828 -0
- data/lib/octo/tools/todo_manager.rb +374 -0
- data/lib/octo/tools/trash_manager.rb +388 -0
- data/lib/octo/tools/undo_task.rb +35 -0
- data/lib/octo/tools/web_fetch.rb +242 -0
- data/lib/octo/tools/web_search.rb +260 -0
- data/lib/octo/tools/write.rb +77 -0
- data/lib/octo/ui2/block_font.rb +10 -0
- data/lib/octo/ui2/components/base_component.rb +163 -0
- data/lib/octo/ui2/components/command_suggestions.rb +290 -0
- data/lib/octo/ui2/components/common_component.rb +96 -0
- data/lib/octo/ui2/components/inline_input.rb +226 -0
- data/lib/octo/ui2/components/input_area.rb +1338 -0
- data/lib/octo/ui2/components/message_component.rb +99 -0
- data/lib/octo/ui2/components/modal_component.rb +419 -0
- data/lib/octo/ui2/components/todo_area.rb +149 -0
- data/lib/octo/ui2/components/tool_component.rb +107 -0
- data/lib/octo/ui2/components/welcome_banner.rb +139 -0
- data/lib/octo/ui2/layout_manager.rb +807 -0
- data/lib/octo/ui2/line_editor.rb +363 -0
- data/lib/octo/ui2/markdown_renderer.rb +100 -0
- data/lib/octo/ui2/output_buffer.rb +370 -0
- data/lib/octo/ui2/progress_handle.rb +362 -0
- data/lib/octo/ui2/progress_indicator.rb +55 -0
- data/lib/octo/ui2/screen_buffer.rb +273 -0
- data/lib/octo/ui2/terminal_detector.rb +119 -0
- data/lib/octo/ui2/theme_manager.rb +85 -0
- data/lib/octo/ui2/themes/base_theme.rb +105 -0
- data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
- data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
- data/lib/octo/ui2/thinking_verbs.rb +26 -0
- data/lib/octo/ui2/ui_controller.rb +1625 -0
- data/lib/octo/ui2/view_renderer.rb +177 -0
- data/lib/octo/ui2.rb +40 -0
- data/lib/octo/ui_interface.rb +154 -0
- data/lib/octo/utils/arguments_parser.rb +191 -0
- data/lib/octo/utils/browser_detector.rb +195 -0
- data/lib/octo/utils/encoding.rb +92 -0
- data/lib/octo/utils/environment_detector.rb +140 -0
- data/lib/octo/utils/file_ignore_helper.rb +170 -0
- data/lib/octo/utils/file_processor.rb +601 -0
- data/lib/octo/utils/gitignore_parser.rb +154 -0
- data/lib/octo/utils/limit_stack.rb +152 -0
- data/lib/octo/utils/logger.rb +124 -0
- data/lib/octo/utils/login_shell.rb +72 -0
- data/lib/octo/utils/model_pricing.rb +646 -0
- data/lib/octo/utils/parser_manager.rb +165 -0
- data/lib/octo/utils/path_helper.rb +15 -0
- data/lib/octo/utils/scripts_manager.rb +59 -0
- data/lib/octo/utils/string_matcher.rb +158 -0
- data/lib/octo/utils/trash_directory.rb +112 -0
- data/lib/octo/utils/workspace_rules.rb +46 -0
- data/lib/octo/version.rb +5 -0
- data/lib/octo/web/app.css +7141 -0
- data/lib/octo/web/app.js +543 -0
- data/lib/octo/web/apple-touch-icon.png +0 -0
- data/lib/octo/web/auth.js +150 -0
- data/lib/octo/web/channels.js +276 -0
- data/lib/octo/web/datepicker.js +205 -0
- data/lib/octo/web/favicon.png +0 -0
- data/lib/octo/web/i18n.js +1073 -0
- data/lib/octo/web/icon-512.png +0 -0
- data/lib/octo/web/icon-dark.svg +25 -0
- data/lib/octo/web/icon.svg +29 -0
- data/lib/octo/web/index.html +871 -0
- data/lib/octo/web/marked.min.js +69 -0
- data/lib/octo/web/onboard.js +491 -0
- data/lib/octo/web/profile.js +442 -0
- data/lib/octo/web/sessions.js +4421 -0
- data/lib/octo/web/settings.js +913 -0
- data/lib/octo/web/sidebar.js +32 -0
- data/lib/octo/web/skills.js +885 -0
- data/lib/octo/web/tasks.js +297 -0
- data/lib/octo/web/theme.js +105 -0
- data/lib/octo/web/trash.js +343 -0
- data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
- data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/katex.min.css +1 -0
- data/lib/octo/web/vendor/katex/katex.min.js +1 -0
- data/lib/octo/web/version.js +449 -0
- data/lib/octo/web/weixin-qr.html +209 -0
- data/lib/octo/web/ws-dispatcher.js +357 -0
- data/lib/octo/web/ws.js +128 -0
- data/lib/octo.rb +145 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +56 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +157 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +174 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.ps1 +532 -0
- data/scripts/install.sh +567 -0
- data/scripts/install_browser.sh +479 -0
- data/scripts/install_full.sh +838 -0
- data/scripts/install_rails_deps.sh +746 -0
- data/scripts/install_system_deps.sh +518 -0
- data/scripts/uninstall.sh +287 -0
- data/sig/octo.rbs +4 -0
- 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.
|