@baimingtao/lbs-agent 1.0.0
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.
- package/AGENTS.md +54 -0
- package/README.md +181 -0
- package/SOUL.md +31 -0
- package/application/__init__.py +1 -0
- package/application/container.py +376 -0
- package/application/contracts/__init__.py +5 -0
- package/application/contracts/result.py +84 -0
- package/application/onboarding.py +243 -0
- package/application/runtime_factory.py +198 -0
- package/application/runtime_paths.py +61 -0
- package/application/services/__init__.py +1 -0
- package/application/services/agent_service.py +159 -0
- package/application/services/cron_service.py +49 -0
- package/application/services/gateway_service.py +156 -0
- package/application/services/interactive_service.py +275 -0
- package/application/services/mcp_service.py +102 -0
- package/application/services/model_service.py +519 -0
- package/application/services/orchestration_service.py +169 -0
- package/application/services/session_service.py +130 -0
- package/application/services/skill_service.py +369 -0
- package/application/services/tool_service.py +165 -0
- package/bin/lbs.js +109 -0
- package/config/__init__.py +2 -0
- package/config/config.py +453 -0
- package/config/config_contract.py +198 -0
- package/config/mcp.yaml +30 -0
- package/config/state_root.py +28 -0
- package/extensions/mcps/optional/linear/manifest.yaml +38 -0
- package/extensions/mcps/optional/n8n/manifest.yaml +77 -0
- package/extensions/mcps/optional/unreal-engine/manifest.yaml +54 -0
- package/extensions/prompts/agents/audit.md +46 -0
- package/extensions/prompts/agents/auditor.md +71 -0
- package/extensions/prompts/agents/browser_worker.md +56 -0
- package/extensions/prompts/agents/chat.md +23 -0
- package/extensions/prompts/agents/code.md +52 -0
- package/extensions/prompts/agents/data_analyst.md +39 -0
- package/extensions/prompts/agents/document.md +19 -0
- package/extensions/prompts/agents/email_agent.md +30 -0
- package/extensions/prompts/agents/github_agent.md +33 -0
- package/extensions/prompts/agents/planner.md +40 -0
- package/extensions/prompts/agents/research.md +50 -0
- package/extensions/prompts/agents/security.md +28 -0
- package/extensions/prompts/agents/verifier.md +28 -0
- package/extensions/prompts/agents/vision.md +27 -0
- package/extensions/skills/builtin/autonomous-ai-agents/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/autonomous-ai-agents/claude-code/SKILL.md +669 -0
- package/extensions/skills/builtin/autonomous-ai-agents/codex/SKILL.md +138 -0
- package/extensions/skills/builtin/autonomous-ai-agents/hermes-agent/SKILL.md +728 -0
- package/extensions/skills/builtin/autonomous-ai-agents/hermes-agent/references/native-mcp.md +344 -0
- package/extensions/skills/builtin/autonomous-ai-agents/hermes-agent/references/webhooks.md +193 -0
- package/extensions/skills/builtin/autonomous-ai-agents/opencode/SKILL.md +219 -0
- package/extensions/skills/builtin/browser-use/SKILL.md +152 -0
- package/extensions/skills/builtin/computer-use/SKILL.md +242 -0
- package/extensions/skills/builtin/creative/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/creative/architecture-diagram/SKILL.md +148 -0
- package/extensions/skills/builtin/creative/architecture-diagram/templates/template.html +319 -0
- package/extensions/skills/builtin/creative/ascii-art/SKILL.md +322 -0
- package/extensions/skills/builtin/creative/ascii-video/README.md +290 -0
- package/extensions/skills/builtin/creative/ascii-video/SKILL.md +241 -0
- package/extensions/skills/builtin/creative/ascii-video/references/architecture.md +568 -0
- package/extensions/skills/builtin/creative/ascii-video/references/composition.md +662 -0
- package/extensions/skills/builtin/creative/ascii-video/references/effects.md +640 -0
- package/extensions/skills/builtin/creative/ascii-video/references/inputs.md +685 -0
- package/extensions/skills/builtin/creative/ascii-video/references/optimization.md +688 -0
- package/extensions/skills/builtin/creative/ascii-video/references/scenes.md +741 -0
- package/extensions/skills/builtin/creative/ascii-video/references/shaders.md +683 -0
- package/extensions/skills/builtin/creative/ascii-video/references/troubleshooting.md +367 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/PORT_NOTES.md +43 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/SKILL.md +237 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/analysis-framework.md +182 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/base-prompt.md +43 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/bento-grid.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/binary-comparison.md +48 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/bridge.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/circular-flow.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/comic-strip.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/comparison-matrix.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/dashboard.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/dense-modules.md +72 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/funnel.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/hierarchical-layers.md +48 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/hub-spoke.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/iceberg.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/isometric-map.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/jigsaw.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/linear-progression.md +48 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/periodic-table.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/story-mountain.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/structural-breakdown.md +48 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/tree-branching.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/venn-diagram.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/layouts/winding-roadmap.md +41 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/structured-content-template.md +244 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/aged-academia.md +36 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/bold-graphic.md +36 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/chalkboard.md +61 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/claymation.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/corporate-memphis.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/craft-handmade.md +44 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/cyberpunk-neon.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/hand-drawn-edu.md +63 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/ikea-manual.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/kawaii.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/knolling.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/lego-brick.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/morandi-journal.md +60 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/origami.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/pixel-art.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/pop-laboratory.md +48 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/retro-pop-grid.md +47 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/storybook-watercolor.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/subway-map.md +29 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/technical-schematic.md +36 -0
- package/extensions/skills/builtin/creative/baoyu-infographic/references/styles/ui-wireframe.md +29 -0
- package/extensions/skills/builtin/creative/claude-design/SKILL.md +591 -0
- package/extensions/skills/builtin/creative/comfyui/SKILL.md +609 -0
- package/extensions/skills/builtin/creative/comfyui/references/official-cli.md +252 -0
- package/extensions/skills/builtin/creative/comfyui/references/rest-api.md +297 -0
- package/extensions/skills/builtin/creative/comfyui/references/template-integrity.md +227 -0
- package/extensions/skills/builtin/creative/comfyui/references/workflow-format.md +205 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/_common.py +835 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/auto_fix_deps.py +225 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/check_deps.py +437 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/comfyui_setup.sh +286 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/extract_schema.py +315 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/fetch_logs.py +157 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/hardware_check.py +497 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/health_check.py +223 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/run_batch.py +243 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/run_workflow.py +796 -0
- package/extensions/skills/builtin/creative/comfyui/scripts/ws_monitor.py +267 -0
- package/extensions/skills/builtin/creative/comfyui/tests/README.md +44 -0
- package/extensions/skills/builtin/creative/comfyui/tests/conftest.py +64 -0
- package/extensions/skills/builtin/creative/comfyui/tests/pytest.ini +5 -0
- package/extensions/skills/builtin/creative/comfyui/tests/test_check_deps.py +68 -0
- package/extensions/skills/builtin/creative/comfyui/tests/test_cloud_integration.py +95 -0
- package/extensions/skills/builtin/creative/comfyui/tests/test_common.py +443 -0
- package/extensions/skills/builtin/creative/comfyui/tests/test_extract_schema.py +184 -0
- package/extensions/skills/builtin/creative/comfyui/tests/test_run_workflow.py +210 -0
- package/extensions/skills/builtin/creative/comfyui/workflows/README.md +86 -0
- package/extensions/skills/builtin/creative/comfyui/workflows/animatediff_video.json +64 -0
- package/extensions/skills/builtin/creative/comfyui/workflows/flux_dev_txt2img.json +78 -0
- package/extensions/skills/builtin/creative/comfyui/workflows/sd15_txt2img.json +49 -0
- package/extensions/skills/builtin/creative/comfyui/workflows/sdxl_img2img.json +54 -0
- package/extensions/skills/builtin/creative/comfyui/workflows/sdxl_inpaint.json +59 -0
- package/extensions/skills/builtin/creative/comfyui/workflows/sdxl_txt2img.json +49 -0
- package/extensions/skills/builtin/creative/comfyui/workflows/upscale_4x.json +27 -0
- package/extensions/skills/builtin/creative/comfyui/workflows/wan_video_t2v.json +69 -0
- package/extensions/skills/builtin/creative/design-md/SKILL.md +171 -0
- package/extensions/skills/builtin/creative/design-md/templates/starter.md +92 -0
- package/extensions/skills/builtin/creative/excalidraw/SKILL.md +195 -0
- package/extensions/skills/builtin/creative/excalidraw/references/colors.md +44 -0
- package/extensions/skills/builtin/creative/excalidraw/references/dark-mode.md +67 -0
- package/extensions/skills/builtin/creative/excalidraw/references/examples.md +140 -0
- package/extensions/skills/builtin/creative/excalidraw/scripts/upload.py +133 -0
- package/extensions/skills/builtin/creative/humanizer/LICENSE +21 -0
- package/extensions/skills/builtin/creative/humanizer/SKILL.md +578 -0
- package/extensions/skills/builtin/creative/manim-video/README.md +23 -0
- package/extensions/skills/builtin/creative/manim-video/SKILL.md +269 -0
- package/extensions/skills/builtin/creative/manim-video/references/animation-design-thinking.md +161 -0
- package/extensions/skills/builtin/creative/manim-video/references/animations.md +282 -0
- package/extensions/skills/builtin/creative/manim-video/references/camera-and-3d.md +135 -0
- package/extensions/skills/builtin/creative/manim-video/references/decorations.md +202 -0
- package/extensions/skills/builtin/creative/manim-video/references/equations.md +216 -0
- package/extensions/skills/builtin/creative/manim-video/references/graphs-and-data.md +163 -0
- package/extensions/skills/builtin/creative/manim-video/references/mobjects.md +333 -0
- package/extensions/skills/builtin/creative/manim-video/references/paper-explainer.md +255 -0
- package/extensions/skills/builtin/creative/manim-video/references/production-quality.md +190 -0
- package/extensions/skills/builtin/creative/manim-video/references/rendering.md +185 -0
- package/extensions/skills/builtin/creative/manim-video/references/scene-planning.md +118 -0
- package/extensions/skills/builtin/creative/manim-video/references/troubleshooting.md +135 -0
- package/extensions/skills/builtin/creative/manim-video/references/updaters-and-trackers.md +260 -0
- package/extensions/skills/builtin/creative/manim-video/references/visual-design.md +124 -0
- package/extensions/skills/builtin/creative/manim-video/scripts/setup.sh +14 -0
- package/extensions/skills/builtin/creative/p5js/README.md +64 -0
- package/extensions/skills/builtin/creative/p5js/SKILL.md +555 -0
- package/extensions/skills/builtin/creative/p5js/references/animation.md +439 -0
- package/extensions/skills/builtin/creative/p5js/references/color-systems.md +352 -0
- package/extensions/skills/builtin/creative/p5js/references/core-api.md +410 -0
- package/extensions/skills/builtin/creative/p5js/references/export-pipeline.md +566 -0
- package/extensions/skills/builtin/creative/p5js/references/interaction.md +398 -0
- package/extensions/skills/builtin/creative/p5js/references/shapes-and-geometry.md +300 -0
- package/extensions/skills/builtin/creative/p5js/references/troubleshooting.md +532 -0
- package/extensions/skills/builtin/creative/p5js/references/typography.md +302 -0
- package/extensions/skills/builtin/creative/p5js/references/visual-effects.md +895 -0
- package/extensions/skills/builtin/creative/p5js/references/webgl-and-3d.md +423 -0
- package/extensions/skills/builtin/creative/p5js/scripts/export-frames.js +179 -0
- package/extensions/skills/builtin/creative/p5js/scripts/render.sh +108 -0
- package/extensions/skills/builtin/creative/p5js/scripts/serve.sh +28 -0
- package/extensions/skills/builtin/creative/p5js/scripts/setup.sh +87 -0
- package/extensions/skills/builtin/creative/p5js/templates/viewer.html +395 -0
- package/extensions/skills/builtin/creative/popular-web-designs/SKILL.md +202 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/airbnb.md +259 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/airtable.md +102 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/apple.md +326 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/bmw.md +193 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/cal.md +272 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/claude.md +325 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/clay.md +317 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/clickhouse.md +294 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/cohere.md +279 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/coinbase.md +142 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/composio.md +320 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/cursor.md +322 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/elevenlabs.md +278 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/expo.md +294 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/figma.md +233 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/framer.md +259 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/hashicorp.md +291 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/ibm.md +345 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/intercom.md +159 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/kraken.md +138 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/linear.app.md +380 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/lovable.md +311 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/minimax.md +270 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/mintlify.md +339 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/miro.md +121 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/mistral.ai.md +274 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/mongodb.md +279 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/notion.md +322 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/nvidia.md +306 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/ollama.md +280 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/opencode.ai.md +294 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/pinterest.md +243 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/posthog.md +269 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/raycast.md +281 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/replicate.md +274 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/resend.md +316 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/revolut.md +198 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/runwayml.md +257 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/sanity.md +370 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/sentry.md +275 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/spacex.md +207 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/spotify.md +259 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/stripe.md +335 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/supabase.md +268 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/superhuman.md +265 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/together.ai.md +276 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/uber.md +308 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/vercel.md +323 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/voltagent.md +336 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/warp.md +265 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/webflow.md +105 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/wise.md +186 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/x.ai.md +270 -0
- package/extensions/skills/builtin/creative/popular-web-designs/templates/zapier.md +341 -0
- package/extensions/skills/builtin/creative/pretext/SKILL.md +220 -0
- package/extensions/skills/builtin/creative/pretext/references/patterns.md +258 -0
- package/extensions/skills/builtin/creative/pretext/templates/donut-orbit.html +1468 -0
- package/extensions/skills/builtin/creative/pretext/templates/hello-orb-flow.html +95 -0
- package/extensions/skills/builtin/creative/sketch/SKILL.md +218 -0
- package/extensions/skills/builtin/creative/songwriting-and-ai-music/SKILL.md +273 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/SKILL.md +356 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/3d-scene.md +275 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/animation.md +221 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/audio-reactive.md +175 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/dat-scripting.md +352 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/external-data.md +322 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/geometry-comp.md +121 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/glsl.md +151 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/layout-compositor.md +131 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/mcp-tools.md +382 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/midi-osc.md +210 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/network-patterns.md +767 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/operator-tips.md +106 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/operators.md +239 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/panel-ui.md +281 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/particles.md +245 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/pitfalls.md +568 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/postfx.md +183 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/projection-mapping.md +211 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/python-api.md +463 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/replicator.md +197 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/references/troubleshooting.md +244 -0
- package/extensions/skills/builtin/creative/touchdesigner-mcp/scripts/setup.sh +115 -0
- package/extensions/skills/builtin/data-science/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/data-science/jupyter-live-kernel/SKILL.md +152 -0
- package/extensions/skills/builtin/dogfood/SKILL.md +162 -0
- package/extensions/skills/builtin/dogfood/references/issue-taxonomy.md +109 -0
- package/extensions/skills/builtin/dogfood/templates/dogfood-report-template.md +86 -0
- package/extensions/skills/builtin/email/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/email/himalaya/SKILL.md +302 -0
- package/extensions/skills/builtin/email/himalaya/references/configuration.md +216 -0
- package/extensions/skills/builtin/email/himalaya/references/message-composition.md +199 -0
- package/extensions/skills/builtin/github/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/github/codebase-inspection/SKILL.md +116 -0
- package/extensions/skills/builtin/github/github-auth/SKILL.md +247 -0
- package/extensions/skills/builtin/github/github-auth/scripts/gh-env.sh +66 -0
- package/extensions/skills/builtin/github/github-code-review/SKILL.md +481 -0
- package/extensions/skills/builtin/github/github-code-review/references/review-output-template.md +74 -0
- package/extensions/skills/builtin/github/github-issues/SKILL.md +370 -0
- package/extensions/skills/builtin/github/github-issues/templates/bug-report.md +35 -0
- package/extensions/skills/builtin/github/github-issues/templates/feature-request.md +31 -0
- package/extensions/skills/builtin/github/github-pr-workflow/SKILL.md +367 -0
- package/extensions/skills/builtin/github/github-pr-workflow/references/ci-troubleshooting.md +183 -0
- package/extensions/skills/builtin/github/github-pr-workflow/references/conventional-commits.md +71 -0
- package/extensions/skills/builtin/github/github-pr-workflow/templates/pr-body-bugfix.md +35 -0
- package/extensions/skills/builtin/github/github-pr-workflow/templates/pr-body-feature.md +33 -0
- package/extensions/skills/builtin/github/github-repo-management/SKILL.md +516 -0
- package/extensions/skills/builtin/github/github-repo-management/references/github-api-cheatsheet.md +161 -0
- package/extensions/skills/builtin/index-cache/anthropics_skills_skills_.json +1 -0
- package/extensions/skills/builtin/index-cache/claude_marketplace_anthropics_skills.json +1 -0
- package/extensions/skills/builtin/index-cache/lobehub_index.json +1 -0
- package/extensions/skills/builtin/index-cache/openai_skills_skills_.json +1 -0
- package/extensions/skills/builtin/media/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/media/gif-search/SKILL.md +91 -0
- package/extensions/skills/builtin/media/heartmula/SKILL.md +171 -0
- package/extensions/skills/builtin/media/songsee/SKILL.md +83 -0
- package/extensions/skills/builtin/media/youtube-content/SKILL.md +75 -0
- package/extensions/skills/builtin/media/youtube-content/references/output-formats.md +56 -0
- package/extensions/skills/builtin/media/youtube-content/scripts/fetch_transcript.py +124 -0
- package/extensions/skills/builtin/mlops/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/mlops/evaluation/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/mlops/evaluation/lm-evaluation-harness/SKILL.md +495 -0
- package/extensions/skills/builtin/mlops/evaluation/lm-evaluation-harness/references/api-evaluation.md +490 -0
- package/extensions/skills/builtin/mlops/evaluation/lm-evaluation-harness/references/benchmark-guide.md +488 -0
- package/extensions/skills/builtin/mlops/evaluation/lm-evaluation-harness/references/custom-tasks.md +602 -0
- package/extensions/skills/builtin/mlops/evaluation/lm-evaluation-harness/references/distributed-eval.md +519 -0
- package/extensions/skills/builtin/mlops/evaluation/weights-and-biases/SKILL.md +592 -0
- package/extensions/skills/builtin/mlops/evaluation/weights-and-biases/references/artifacts.md +584 -0
- package/extensions/skills/builtin/mlops/evaluation/weights-and-biases/references/integrations.md +700 -0
- package/extensions/skills/builtin/mlops/evaluation/weights-and-biases/references/sweeps.md +846 -0
- package/extensions/skills/builtin/mlops/huggingface-hub/SKILL.md +81 -0
- package/extensions/skills/builtin/mlops/inference/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/mlops/inference/llama-cpp/SKILL.md +249 -0
- package/extensions/skills/builtin/mlops/inference/llama-cpp/references/advanced-usage.md +504 -0
- package/extensions/skills/builtin/mlops/inference/llama-cpp/references/hub-discovery.md +168 -0
- package/extensions/skills/builtin/mlops/inference/llama-cpp/references/optimization.md +89 -0
- package/extensions/skills/builtin/mlops/inference/llama-cpp/references/quantization.md +243 -0
- package/extensions/skills/builtin/mlops/inference/llama-cpp/references/server.md +150 -0
- package/extensions/skills/builtin/mlops/inference/llama-cpp/references/troubleshooting.md +442 -0
- package/extensions/skills/builtin/mlops/inference/vllm/SKILL.md +369 -0
- package/extensions/skills/builtin/mlops/inference/vllm/references/optimization.md +226 -0
- package/extensions/skills/builtin/mlops/inference/vllm/references/quantization.md +284 -0
- package/extensions/skills/builtin/mlops/inference/vllm/references/server-deployment.md +255 -0
- package/extensions/skills/builtin/mlops/inference/vllm/references/troubleshooting.md +448 -0
- package/extensions/skills/builtin/mlops/models/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/mlops/models/audiocraft/SKILL.md +568 -0
- package/extensions/skills/builtin/mlops/models/audiocraft/references/advanced-usage.md +666 -0
- package/extensions/skills/builtin/mlops/models/audiocraft/references/troubleshooting.md +504 -0
- package/extensions/skills/builtin/mlops/models/segment-anything/SKILL.md +506 -0
- package/extensions/skills/builtin/mlops/models/segment-anything/references/advanced-usage.md +589 -0
- package/extensions/skills/builtin/mlops/models/segment-anything/references/troubleshooting.md +484 -0
- package/extensions/skills/builtin/note-taking/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/note-taking/obsidian/SKILL.md +61 -0
- package/extensions/skills/builtin/productivity/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/productivity/airtable/SKILL.md +229 -0
- package/extensions/skills/builtin/productivity/google-workspace/SKILL.md +314 -0
- package/extensions/skills/builtin/productivity/google-workspace/references/gmail-search-syntax.md +63 -0
- package/extensions/skills/builtin/productivity/google-workspace/scripts/_hermes_home.py +42 -0
- package/extensions/skills/builtin/productivity/google-workspace/scripts/google_api.py +1225 -0
- package/extensions/skills/builtin/productivity/google-workspace/scripts/gws_bridge.py +111 -0
- package/extensions/skills/builtin/productivity/google-workspace/scripts/setup.py +481 -0
- package/extensions/skills/builtin/productivity/maps/SKILL.md +182 -0
- package/extensions/skills/builtin/productivity/maps/scripts/maps_client.py +1297 -0
- package/extensions/skills/builtin/productivity/nano-pdf/SKILL.md +52 -0
- package/extensions/skills/builtin/productivity/notion/SKILL.md +448 -0
- package/extensions/skills/builtin/productivity/notion/references/block-types.md +112 -0
- package/extensions/skills/builtin/productivity/ocr-and-documents/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/productivity/ocr-and-documents/SKILL.md +172 -0
- package/extensions/skills/builtin/productivity/ocr-and-documents/scripts/extract_marker.py +87 -0
- package/extensions/skills/builtin/productivity/ocr-and-documents/scripts/extract_pymupdf.py +98 -0
- package/extensions/skills/builtin/productivity/petdex/SKILL.md +76 -0
- package/extensions/skills/builtin/productivity/powerpoint/LICENSE.txt +30 -0
- package/extensions/skills/builtin/productivity/powerpoint/SKILL.md +237 -0
- package/extensions/skills/builtin/productivity/powerpoint/editing.md +205 -0
- package/extensions/skills/builtin/productivity/powerpoint/pptxgenjs.md +420 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/__init__.py +0 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/add_slide.py +195 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/clean.py +286 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/helpers/__init__.py +0 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/helpers/merge_runs.py +199 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/helpers/simplify_redlines.py +197 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/pack.py +159 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-contentTypes.xsd +42 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-coreProperties.xsd +50 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-digSig.xsd +49 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-relationships.xsd +33 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/mce/mc.xsd +75 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/extensions/skills/builtin/productivity/powerpoint/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/extensions/skills/builtin/productivity/teams-meeting-pipeline/SKILL.md +116 -0
- package/extensions/skills/builtin/research/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/research/arxiv/SKILL.md +282 -0
- package/extensions/skills/builtin/research/arxiv/scripts/search_arxiv.py +114 -0
- package/extensions/skills/builtin/research/blogwatcher/SKILL.md +137 -0
- package/extensions/skills/builtin/research/llm-wiki/SKILL.md +449 -0
- package/extensions/skills/builtin/research/polymarket/SKILL.md +75 -0
- package/extensions/skills/builtin/research/polymarket/references/api-endpoints.md +220 -0
- package/extensions/skills/builtin/research/polymarket/scripts/polymarket.py +284 -0
- package/extensions/skills/builtin/research/research-paper-writing/SKILL.md +813 -0
- package/extensions/skills/builtin/research/research-paper-writing/references/autoreason-methodology.md +394 -0
- package/extensions/skills/builtin/research/research-paper-writing/references/checklists.md +434 -0
- package/extensions/skills/builtin/research/research-paper-writing/references/citation-workflow.md +564 -0
- package/extensions/skills/builtin/research/research-paper-writing/references/experiment-patterns.md +728 -0
- package/extensions/skills/builtin/research/research-paper-writing/references/human-evaluation.md +476 -0
- package/extensions/skills/builtin/research/research-paper-writing/references/paper-types.md +481 -0
- package/extensions/skills/builtin/research/research-paper-writing/references/reviewer-guidelines.md +433 -0
- package/extensions/skills/builtin/research/research-paper-writing/references/sources.md +191 -0
- package/extensions/skills/builtin/research/research-paper-writing/references/writing-guide.md +474 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/README.md +251 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/aaai2026/README.md +534 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +952 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/aaai2026/aaai2026.bib +111 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/aaai2026/aaai2026.bst +1493 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/aaai2026/aaai2026.sty +315 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/acl/README.md +48 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/acl/acl.sty +312 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/acl/acl_latex.tex +377 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/acl/acl_lualatex.tex +101 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/acl/acl_natbib.bst +1940 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/acl/anthology.bib.txt +26 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/acl/custom.bib +70 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/acl/formatting.md +322 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/colm2025/README.md +3 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/colm2025/colm2025_conference.bib +11 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/colm2025/colm2025_conference.bst +1440 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/colm2025/colm2025_conference.sty +218 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/colm2025/colm2025_conference.tex +305 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/colm2025/fancyhdr.sty +485 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/colm2025/math_commands.tex +508 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/colm2025/natbib.sty +1246 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/iclr2026/fancyhdr.sty +485 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/iclr2026/iclr2026_conference.bib +24 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/iclr2026/iclr2026_conference.bst +1440 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/iclr2026/iclr2026_conference.sty +246 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/iclr2026/iclr2026_conference.tex +414 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/iclr2026/math_commands.tex +508 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/iclr2026/natbib.sty +1246 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/icml2026/algorithm.sty +79 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/icml2026/algorithmic.sty +201 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/icml2026/example_paper.bib +75 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/icml2026/example_paper.pdf +0 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/icml2026/example_paper.tex +662 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/icml2026/fancyhdr.sty +864 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/icml2026/icml2026.bst +1443 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/icml2026/icml2026.sty +767 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/neurips2025/Makefile +36 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/neurips2025/extra_pkgs.tex +53 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/neurips2025/main.tex +38 -0
- package/extensions/skills/builtin/research/research-paper-writing/templates/neurips2025/neurips.sty +382 -0
- package/extensions/skills/builtin/research/web-research/SKILL.md +10 -0
- package/extensions/skills/builtin/smart-home/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/smart-home/openhue/SKILL.md +109 -0
- package/extensions/skills/builtin/social-media/DESCRIPTION.md +3 -0
- package/extensions/skills/builtin/social-media/xurl/SKILL.md +429 -0
- package/extensions/skills/builtin/software-development/hermes-agent-skill-authoring/SKILL.md +196 -0
- package/extensions/skills/builtin/software-development/node-inspect-debugger/SKILL.md +319 -0
- package/extensions/skills/builtin/software-development/plan/SKILL.md +338 -0
- package/extensions/skills/builtin/software-development/python-debugpy/SKILL.md +375 -0
- package/extensions/skills/builtin/software-development/requesting-code-review/SKILL.md +275 -0
- package/extensions/skills/builtin/software-development/simplify-code/SKILL.md +118 -0
- package/extensions/skills/builtin/software-development/spike/SKILL.md +197 -0
- package/extensions/skills/builtin/software-development/systematic-debugging/SKILL.md +411 -0
- package/extensions/skills/builtin/software-development/test-driven-development/SKILL.md +362 -0
- package/extensions/skills/builtin/yuanbao/SKILL.md +108 -0
- package/extensions/skills/optional/DESCRIPTION.md +22 -0
- package/extensions/skills/optional/autonomous-ai-agents/DESCRIPTION.md +1 -0
- package/extensions/skills/optional/autonomous-ai-agents/antigravity-cli/SKILL.md +197 -0
- package/extensions/skills/optional/autonomous-ai-agents/antigravity-cli/references/cli-docs.md +64 -0
- package/extensions/skills/optional/autonomous-ai-agents/blackbox/SKILL.md +144 -0
- package/extensions/skills/optional/autonomous-ai-agents/grok/SKILL.md +246 -0
- package/extensions/skills/optional/autonomous-ai-agents/honcho/SKILL.md +431 -0
- package/extensions/skills/optional/autonomous-ai-agents/openhands/SKILL.md +149 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/PORT_NOTES.md +48 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/SKILL.md +207 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/prompts/system.md +32 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/palettes/macaron.md +33 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +42 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/palettes/neon.md +33 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/palettes/warm.md +32 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/prompt-construction.md +426 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/style-presets.md +80 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/blueprint.md +57 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/chalkboard.md +62 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/editorial.md +59 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/elegant.md +56 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +58 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +61 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/flat.md +59 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/ink-notes.md +90 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +57 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/minimal.md +58 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/nature.md +58 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/notion.md +58 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/pixel-art.md +57 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/playful.md +59 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/retro.md +59 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/scientific.md +59 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/screen-print.md +70 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +56 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/sketch.md +57 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +57 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/vintage.md +59 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/warm.md +58 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles/watercolor.md +58 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/styles.md +223 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/usage.md +50 -0
- package/extensions/skills/optional/creative/baoyu-article-illustrator/references/workflow.md +332 -0
- package/extensions/skills/optional/creative/baoyu-comic/PORT_NOTES.md +77 -0
- package/extensions/skills/optional/creative/baoyu-comic/SKILL.md +247 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/analysis-framework.md +176 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/art-styles/chalk.md +101 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/art-styles/ink-brush.md +97 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/art-styles/ligne-claire.md +75 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/art-styles/manga.md +93 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/art-styles/minimalist.md +84 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/art-styles/realistic.md +89 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/auto-selection.md +71 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/base-prompt.md +98 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/character-template.md +180 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/layouts/cinematic.md +23 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/layouts/dense.md +23 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/layouts/four-panel.md +40 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/layouts/mixed.md +23 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/layouts/splash.md +23 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/layouts/standard.md +23 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/layouts/webtoon.md +30 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/ohmsha-guide.md +85 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/partial-workflows.md +106 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/presets/concept-story.md +121 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/presets/four-panel.md +107 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/presets/ohmsha.md +114 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/presets/shoujo.md +116 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/presets/wuxia.md +110 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/storyboard-template.md +142 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/tones/action.md +110 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/tones/dramatic.md +95 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/tones/energetic.md +105 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/tones/neutral.md +63 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/tones/romantic.md +100 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/tones/vintage.md +104 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/tones/warm.md +94 -0
- package/extensions/skills/optional/creative/baoyu-comic/references/workflow.md +401 -0
- package/extensions/skills/optional/creative/blender-mcp/SKILL.md +117 -0
- package/extensions/skills/optional/creative/concept-diagrams/SKILL.md +362 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/electricity-grid-flow.md +182 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
- package/extensions/skills/optional/creative/concept-diagrams/examples/wind-turbine-structure.md +338 -0
- package/extensions/skills/optional/creative/concept-diagrams/references/dashboard-patterns.md +43 -0
- package/extensions/skills/optional/creative/concept-diagrams/references/infrastructure-patterns.md +144 -0
- package/extensions/skills/optional/creative/concept-diagrams/references/physical-shape-cookbook.md +42 -0
- package/extensions/skills/optional/creative/concept-diagrams/templates/template.html +174 -0
- package/extensions/skills/optional/creative/creative-ideation/SKILL.md +177 -0
- package/extensions/skills/optional/creative/creative-ideation/references/anti-slop.md +106 -0
- package/extensions/skills/optional/creative/creative-ideation/references/exercises.md +71 -0
- package/extensions/skills/optional/creative/creative-ideation/references/full-prompt-library.md +180 -0
- package/extensions/skills/optional/creative/creative-ideation/references/heuristics.md +85 -0
- package/extensions/skills/optional/creative/creative-ideation/references/method-catalog.md +88 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/affinity-diagrams.md +67 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/analogy-and-blending.md +83 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/biomimicry.md +58 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/chance-and-remix.md +75 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/compression-progress.md +64 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/creative-discipline.md +82 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/defamiliarization.md +58 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/derive-and-mapping.md +76 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/first-principles.md +63 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/jobs-to-be-done.md +73 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/lateral-provocations.md +81 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/leverage-points.md +70 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/oblique-strategies.md +87 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/oulipo.md +75 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/pataphysics.md +64 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/pattern-languages.md +78 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/polya.md +77 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/premortem-and-inversion.md +71 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/scamper.md +63 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/story-skeletons.md +100 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/triz-principles.md +95 -0
- package/extensions/skills/optional/creative/creative-ideation/references/methods/volume-generation.md +74 -0
- package/extensions/skills/optional/creative/hyperframes/SKILL.md +191 -0
- package/extensions/skills/optional/creative/hyperframes/references/cli.md +185 -0
- package/extensions/skills/optional/creative/hyperframes/references/composition.md +129 -0
- package/extensions/skills/optional/creative/hyperframes/references/features.md +289 -0
- package/extensions/skills/optional/creative/hyperframes/references/gsap.md +136 -0
- package/extensions/skills/optional/creative/hyperframes/references/troubleshooting.md +137 -0
- package/extensions/skills/optional/creative/hyperframes/references/website-to-video.md +145 -0
- package/extensions/skills/optional/creative/hyperframes/scripts/setup.sh +135 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/SKILL.md +158 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/assets/brief.md.tmpl +79 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +186 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/assets/soul.md.tmpl +38 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/references/examples.md +203 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/references/intake.md +142 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/references/kanban-setup.md +271 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/references/monitoring.md +155 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/references/role-archetypes.md +234 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/references/tool-matrix.md +281 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/scripts/bootstrap_pipeline.py +499 -0
- package/extensions/skills/optional/creative/kanban-video-orchestrator/scripts/monitor.py +195 -0
- package/extensions/skills/optional/creative/meme-generation/EXAMPLES.md +46 -0
- package/extensions/skills/optional/creative/meme-generation/SKILL.md +130 -0
- package/extensions/skills/optional/creative/meme-generation/scripts/generate_meme.py +470 -0
- package/extensions/skills/optional/creative/meme-generation/scripts/templates.json +97 -0
- package/extensions/skills/optional/creative/pixel-art/ATTRIBUTION.md +48 -0
- package/extensions/skills/optional/creative/pixel-art/SKILL.md +199 -0
- package/extensions/skills/optional/creative/pixel-art/references/palettes.md +49 -0
- package/extensions/skills/optional/creative/pixel-art/scripts/__init__.py +0 -0
- package/extensions/skills/optional/creative/pixel-art/scripts/palettes.py +167 -0
- package/extensions/skills/optional/creative/pixel-art/scripts/pixel_art.py +162 -0
- package/extensions/skills/optional/creative/pixel-art/scripts/pixel_art_video.py +345 -0
- package/extensions/skills/optional/devops/cli/SKILL.md +156 -0
- package/extensions/skills/optional/devops/cli/references/app-discovery.md +111 -0
- package/extensions/skills/optional/devops/cli/references/authentication.md +59 -0
- package/extensions/skills/optional/devops/cli/references/cli-reference.md +104 -0
- package/extensions/skills/optional/devops/cli/references/running-apps.md +171 -0
- package/extensions/skills/optional/devops/docker-management/SKILL.md +281 -0
- package/extensions/skills/optional/devops/hermes-s6-container-supervision/SKILL.md +178 -0
- package/extensions/skills/optional/devops/pinggy-tunnel/SKILL.md +309 -0
- package/extensions/skills/optional/devops/watchers/SKILL.md +111 -0
- package/extensions/skills/optional/devops/watchers/scripts/_watermark.py +148 -0
- package/extensions/skills/optional/devops/watchers/scripts/watch_github.py +169 -0
- package/extensions/skills/optional/devops/watchers/scripts/watch_http_json.py +131 -0
- package/extensions/skills/optional/devops/watchers/scripts/watch_rss.py +121 -0
- package/extensions/skills/optional/dogfood/DESCRIPTION.md +3 -0
- package/extensions/skills/optional/dogfood/adversarial-ux-test/SKILL.md +191 -0
- package/extensions/skills/optional/finance/3-statement-model/SKILL.md +430 -0
- package/extensions/skills/optional/finance/3-statement-model/references/formatting.md +118 -0
- package/extensions/skills/optional/finance/3-statement-model/references/formulas.md +292 -0
- package/extensions/skills/optional/finance/3-statement-model/references/sec-filings.md +125 -0
- package/extensions/skills/optional/finance/comps-analysis/SKILL.md +662 -0
- package/extensions/skills/optional/finance/dcf-model/SKILL.md +850 -0
- package/extensions/skills/optional/finance/dcf-model/TROUBLESHOOTING.md +40 -0
- package/extensions/skills/optional/finance/dcf-model/requirements.txt +7 -0
- package/extensions/skills/optional/finance/dcf-model/scripts/validate_dcf.py +291 -0
- package/extensions/skills/optional/finance/excel-author/SKILL.md +244 -0
- package/extensions/skills/optional/finance/excel-author/scripts/recalc.py +88 -0
- package/extensions/skills/optional/finance/lbo-model/SKILL.md +290 -0
- package/extensions/skills/optional/finance/merger-model/SKILL.md +144 -0
- package/extensions/skills/optional/finance/pptx-author/SKILL.md +173 -0
- package/extensions/skills/optional/finance/stocks/SKILL.md +88 -0
- package/extensions/skills/optional/finance/stocks/scripts/stocks_client.py +755 -0
- package/extensions/skills/optional/mcp/DESCRIPTION.md +3 -0
- package/extensions/skills/optional/mcp/fastmcp/SKILL.md +300 -0
- package/extensions/skills/optional/mcp/fastmcp/references/fastmcp-cli.md +110 -0
- package/extensions/skills/optional/mcp/fastmcp/scripts/scaffold_fastmcp.py +56 -0
- package/extensions/skills/optional/mcp/fastmcp/templates/api_wrapper.py +54 -0
- package/extensions/skills/optional/mcp/fastmcp/templates/database_server.py +77 -0
- package/extensions/skills/optional/mcp/fastmcp/templates/file_processor.py +55 -0
- package/extensions/skills/optional/mcp/mcporter/SKILL.md +123 -0
- package/extensions/skills/optional/migration/DESCRIPTION.md +1 -0
- package/extensions/skills/optional/migration/openclaw-migration/SKILL.md +298 -0
- package/extensions/skills/optional/migration/openclaw-migration/scripts/openclaw_to_hermes.py +3136 -0
- package/extensions/skills/optional/mlops/accelerate/SKILL.md +333 -0
- package/extensions/skills/optional/mlops/accelerate/references/custom-plugins.md +453 -0
- package/extensions/skills/optional/mlops/accelerate/references/megatron-integration.md +489 -0
- package/extensions/skills/optional/mlops/accelerate/references/performance.md +525 -0
- package/extensions/skills/optional/mlops/chroma/SKILL.md +408 -0
- package/extensions/skills/optional/mlops/chroma/references/integration.md +38 -0
- package/extensions/skills/optional/mlops/clip/SKILL.md +255 -0
- package/extensions/skills/optional/mlops/clip/references/applications.md +207 -0
- package/extensions/skills/optional/mlops/faiss/SKILL.md +223 -0
- package/extensions/skills/optional/mlops/faiss/references/index_types.md +280 -0
- package/extensions/skills/optional/mlops/flash-attention/SKILL.md +364 -0
- package/extensions/skills/optional/mlops/flash-attention/references/benchmarks.md +215 -0
- package/extensions/skills/optional/mlops/flash-attention/references/transformers-integration.md +293 -0
- package/extensions/skills/optional/mlops/guidance/SKILL.md +574 -0
- package/extensions/skills/optional/mlops/guidance/references/backends.md +553 -0
- package/extensions/skills/optional/mlops/guidance/references/constraints.md +674 -0
- package/extensions/skills/optional/mlops/guidance/references/examples.md +767 -0
- package/extensions/skills/optional/mlops/huggingface-tokenizers/SKILL.md +518 -0
- package/extensions/skills/optional/mlops/huggingface-tokenizers/references/algorithms.md +653 -0
- package/extensions/skills/optional/mlops/huggingface-tokenizers/references/integration.md +637 -0
- package/extensions/skills/optional/mlops/huggingface-tokenizers/references/pipeline.md +723 -0
- package/extensions/skills/optional/mlops/huggingface-tokenizers/references/training.md +565 -0
- package/extensions/skills/optional/mlops/inference/outlines/SKILL.md +654 -0
- package/extensions/skills/optional/mlops/inference/outlines/references/backends.md +615 -0
- package/extensions/skills/optional/mlops/inference/outlines/references/examples.md +773 -0
- package/extensions/skills/optional/mlops/inference/outlines/references/json_generation.md +649 -0
- package/extensions/skills/optional/mlops/instructor/SKILL.md +742 -0
- package/extensions/skills/optional/mlops/instructor/references/examples.md +107 -0
- package/extensions/skills/optional/mlops/instructor/references/providers.md +70 -0
- package/extensions/skills/optional/mlops/instructor/references/validation.md +606 -0
- package/extensions/skills/optional/mlops/lambda-labs/SKILL.md +549 -0
- package/extensions/skills/optional/mlops/lambda-labs/references/advanced-usage.md +611 -0
- package/extensions/skills/optional/mlops/lambda-labs/references/troubleshooting.md +530 -0
- package/extensions/skills/optional/mlops/llava/SKILL.md +306 -0
- package/extensions/skills/optional/mlops/llava/references/training.md +197 -0
- package/extensions/skills/optional/mlops/modal/SKILL.md +345 -0
- package/extensions/skills/optional/mlops/modal/references/advanced-usage.md +503 -0
- package/extensions/skills/optional/mlops/modal/references/troubleshooting.md +494 -0
- package/extensions/skills/optional/mlops/nemo-curator/SKILL.md +384 -0
- package/extensions/skills/optional/mlops/nemo-curator/references/deduplication.md +87 -0
- package/extensions/skills/optional/mlops/nemo-curator/references/filtering.md +102 -0
- package/extensions/skills/optional/mlops/obliteratus/SKILL.md +342 -0
- package/extensions/skills/optional/mlops/obliteratus/references/analysis-modules.md +160 -0
- package/extensions/skills/optional/mlops/obliteratus/references/methods-guide.md +139 -0
- package/extensions/skills/optional/mlops/obliteratus/templates/abliteration-config.yaml +33 -0
- package/extensions/skills/optional/mlops/obliteratus/templates/analysis-study.yaml +40 -0
- package/extensions/skills/optional/mlops/obliteratus/templates/batch-abliteration.yaml +41 -0
- package/extensions/skills/optional/mlops/peft/SKILL.md +435 -0
- package/extensions/skills/optional/mlops/peft/references/advanced-usage.md +514 -0
- package/extensions/skills/optional/mlops/peft/references/troubleshooting.md +480 -0
- package/extensions/skills/optional/mlops/pinecone/SKILL.md +360 -0
- package/extensions/skills/optional/mlops/pinecone/references/deployment.md +181 -0
- package/extensions/skills/optional/mlops/pytorch-fsdp/SKILL.md +85 -0
- package/extensions/skills/optional/mlops/pytorch-fsdp/references/index.md +7 -0
- package/extensions/skills/optional/mlops/pytorch-fsdp/references/other.md +3297 -0
- package/extensions/skills/optional/mlops/pytorch-lightning/SKILL.md +348 -0
- package/extensions/skills/optional/mlops/pytorch-lightning/references/callbacks.md +436 -0
- package/extensions/skills/optional/mlops/pytorch-lightning/references/distributed.md +490 -0
- package/extensions/skills/optional/mlops/pytorch-lightning/references/hyperparameter-tuning.md +556 -0
- package/extensions/skills/optional/mlops/qdrant/SKILL.md +497 -0
- package/extensions/skills/optional/mlops/qdrant/references/advanced-usage.md +648 -0
- package/extensions/skills/optional/mlops/qdrant/references/troubleshooting.md +631 -0
- package/extensions/skills/optional/mlops/research/DESCRIPTION.md +3 -0
- package/extensions/skills/optional/mlops/research/dspy/SKILL.md +592 -0
- package/extensions/skills/optional/mlops/research/dspy/references/examples.md +663 -0
- package/extensions/skills/optional/mlops/research/dspy/references/modules.md +475 -0
- package/extensions/skills/optional/mlops/research/dspy/references/optimizers.md +566 -0
- package/extensions/skills/optional/mlops/saelens/SKILL.md +390 -0
- package/extensions/skills/optional/mlops/saelens/references/README.md +69 -0
- package/extensions/skills/optional/mlops/saelens/references/api.md +333 -0
- package/extensions/skills/optional/mlops/saelens/references/tutorials.md +318 -0
- package/extensions/skills/optional/mlops/simpo/SKILL.md +220 -0
- package/extensions/skills/optional/mlops/simpo/references/datasets.md +478 -0
- package/extensions/skills/optional/mlops/simpo/references/hyperparameters.md +452 -0
- package/extensions/skills/optional/mlops/simpo/references/loss-functions.md +350 -0
- package/extensions/skills/optional/mlops/slime/SKILL.md +467 -0
- package/extensions/skills/optional/mlops/slime/references/api-reference.md +392 -0
- package/extensions/skills/optional/mlops/slime/references/troubleshooting.md +386 -0
- package/extensions/skills/optional/mlops/stable-diffusion/SKILL.md +523 -0
- package/extensions/skills/optional/mlops/stable-diffusion/references/advanced-usage.md +716 -0
- package/extensions/skills/optional/mlops/stable-diffusion/references/troubleshooting.md +555 -0
- package/extensions/skills/optional/mlops/tensorrt-llm/SKILL.md +189 -0
- package/extensions/skills/optional/mlops/tensorrt-llm/references/multi-gpu.md +298 -0
- package/extensions/skills/optional/mlops/tensorrt-llm/references/optimization.md +242 -0
- package/extensions/skills/optional/mlops/tensorrt-llm/references/serving.md +470 -0
- package/extensions/skills/optional/mlops/torchtitan/SKILL.md +361 -0
- package/extensions/skills/optional/mlops/torchtitan/references/checkpoint.md +181 -0
- package/extensions/skills/optional/mlops/torchtitan/references/custom-models.md +258 -0
- package/extensions/skills/optional/mlops/torchtitan/references/float8.md +133 -0
- package/extensions/skills/optional/mlops/torchtitan/references/fsdp.md +126 -0
- package/extensions/skills/optional/mlops/training/axolotl/SKILL.md +164 -0
- package/extensions/skills/optional/mlops/training/axolotl/references/api.md +1535 -0
- package/extensions/skills/optional/mlops/training/axolotl/references/dataset-formats.md +843 -0
- package/extensions/skills/optional/mlops/training/axolotl/references/index.md +15 -0
- package/extensions/skills/optional/mlops/training/axolotl/references/other.md +2367 -0
- package/extensions/skills/optional/mlops/training/trl-fine-tuning/SKILL.md +460 -0
- package/extensions/skills/optional/mlops/training/trl-fine-tuning/references/dpo-variants.md +227 -0
- package/extensions/skills/optional/mlops/training/trl-fine-tuning/references/grpo-training.md +504 -0
- package/extensions/skills/optional/mlops/training/trl-fine-tuning/references/online-rl.md +82 -0
- package/extensions/skills/optional/mlops/training/trl-fine-tuning/references/reward-modeling.md +122 -0
- package/extensions/skills/optional/mlops/training/trl-fine-tuning/references/sft-training.md +168 -0
- package/extensions/skills/optional/mlops/training/trl-fine-tuning/templates/basic_grpo_training.py +228 -0
- package/extensions/skills/optional/mlops/training/unsloth/SKILL.md +81 -0
- package/extensions/skills/optional/mlops/training/unsloth/references/index.md +7 -0
- package/extensions/skills/optional/mlops/training/unsloth/references/llms-full.md +381 -0
- package/extensions/skills/optional/mlops/training/unsloth/references/llms-txt.md +382 -0
- package/extensions/skills/optional/mlops/training/unsloth/references/llms.md +82 -0
- package/extensions/skills/optional/mlops/whisper/SKILL.md +319 -0
- package/extensions/skills/optional/mlops/whisper/references/languages.md +189 -0
- package/extensions/skills/optional/productivity/canvas/SKILL.md +98 -0
- package/extensions/skills/optional/productivity/canvas/scripts/canvas_api.py +160 -0
- package/extensions/skills/optional/productivity/here-now/SKILL.md +217 -0
- package/extensions/skills/optional/productivity/here-now/scripts/drive.sh +406 -0
- package/extensions/skills/optional/productivity/here-now/scripts/publish.sh +445 -0
- package/extensions/skills/optional/productivity/memento-flashcards/SKILL.md +324 -0
- package/extensions/skills/optional/productivity/memento-flashcards/scripts/memento_cards.py +353 -0
- package/extensions/skills/optional/productivity/memento-flashcards/scripts/youtube_quiz.py +88 -0
- package/extensions/skills/optional/productivity/shop/SKILL.md +224 -0
- package/extensions/skills/optional/productivity/shop/references/catalog-mcp.md +236 -0
- package/extensions/skills/optional/productivity/shop/references/direct-api.md +265 -0
- package/extensions/skills/optional/productivity/shop/references/legal.md +3 -0
- package/extensions/skills/optional/productivity/shop/references/safety.md +36 -0
- package/extensions/skills/optional/productivity/shopify/SKILL.md +373 -0
- package/extensions/skills/optional/productivity/siyuan/SKILL.md +298 -0
- package/extensions/skills/optional/productivity/telephony/SKILL.md +418 -0
- package/extensions/skills/optional/productivity/telephony/scripts/telephony.py +1343 -0
- package/extensions/skills/optional/research/bioinformatics/SKILL.md +235 -0
- package/extensions/skills/optional/research/darwinian-evolver/SKILL.md +161 -0
- package/extensions/skills/optional/research/darwinian-evolver/scripts/parrot_openrouter.py +218 -0
- package/extensions/skills/optional/research/darwinian-evolver/scripts/show_snapshot.py +92 -0
- package/extensions/skills/optional/research/darwinian-evolver/templates/custom_problem_template.py +240 -0
- package/extensions/skills/optional/research/domain-intel/SKILL.md +97 -0
- package/extensions/skills/optional/research/domain-intel/scripts/domain_intel.py +397 -0
- package/extensions/skills/optional/research/drug-discovery/SKILL.md +218 -0
- package/extensions/skills/optional/research/drug-discovery/references/ADMET_REFERENCE.md +66 -0
- package/extensions/skills/optional/research/drug-discovery/scripts/chembl_target.py +53 -0
- package/extensions/skills/optional/research/drug-discovery/scripts/ro5_screen.py +44 -0
- package/extensions/skills/optional/research/duckduckgo-search/SKILL.md +238 -0
- package/extensions/skills/optional/research/duckduckgo-search/scripts/duckduckgo.sh +28 -0
- package/extensions/skills/optional/research/gitnexus-explorer/SKILL.md +196 -0
- package/extensions/skills/optional/research/gitnexus-explorer/scripts/proxy.mjs +92 -0
- package/extensions/skills/optional/research/osint-investigation/SKILL.md +227 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/courtlistener.md +92 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/gdelt.md +94 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/icij-offshore.md +79 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/nyc-acris.md +85 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/ofac-sdn.md +84 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/opencorporates.md +88 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/sec-edgar.md +80 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/senate-ld.md +85 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/usaspending.md +90 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/wayback.md +87 -0
- package/extensions/skills/optional/research/osint-investigation/references/sources/wikipedia.md +96 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/_http.py +82 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/_normalize.py +67 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/build_findings.py +221 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/entity_resolution.py +228 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_courtlistener.py +149 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_gdelt.py +161 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_icij_offshore.py +234 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_nyc_acris.py +203 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_ofac_sdn.py +175 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_opencorporates.py +191 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_sec_edgar.py +184 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_senate_ld.py +146 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_usaspending.py +170 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_wayback.py +142 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/fetch_wikipedia.py +266 -0
- package/extensions/skills/optional/research/osint-investigation/scripts/timing_analysis.py +252 -0
- package/extensions/skills/optional/research/osint-investigation/templates/source-template.md +57 -0
- package/extensions/skills/optional/research/parallel-cli/SKILL.md +391 -0
- package/extensions/skills/optional/research/qmd/SKILL.md +417 -0
- package/extensions/skills/optional/research/scrapling/SKILL.md +336 -0
- package/extensions/skills/optional/research/searxng-search/SKILL.md +212 -0
- package/extensions/skills/optional/research/searxng-search/scripts/searxng.sh +22 -0
- package/extensions/skills/optional/security/DESCRIPTION.md +3 -0
- package/extensions/skills/optional/software-development/code-wiki/SKILL.md +442 -0
- package/extensions/skills/optional/software-development/code-wiki/templates/README.md +31 -0
- package/extensions/skills/optional/software-development/code-wiki/templates/architecture.md +30 -0
- package/extensions/skills/optional/software-development/code-wiki/templates/getting-started.md +47 -0
- package/extensions/skills/optional/software-development/code-wiki/templates/module.md +38 -0
- package/extensions/skills/optional/software-development/rest-graphql-debug/SKILL.md +514 -0
- package/extensions/skills/optional/software-development/subagent-driven-development/SKILL.md +352 -0
- package/extensions/skills/optional/software-development/subagent-driven-development/references/context-budget-discipline.md +53 -0
- package/extensions/skills/optional/software-development/subagent-driven-development/references/gates-taxonomy.md +93 -0
- package/extensions/skills/optional/web-development/DESCRIPTION.md +5 -0
- package/extensions/skills/optional/web-development/cloudflare-temporary-deploy/SKILL.md +127 -0
- package/extensions/skills/optional/web-development/cloudflare-temporary-deploy/scripts/parse_deploy_output.py +122 -0
- package/extensions/skills/optional/web-development/page-agent/SKILL.md +190 -0
- package/interfaces/__init__.py +1 -0
- package/interfaces/api/__init__.py +1 -0
- package/interfaces/api/_impl/_models.py +105 -0
- package/interfaces/api/_impl/server.py +1480 -0
- package/interfaces/cli/__init__.py +1 -0
- package/interfaces/cli/_impl/__init__.py +1 -0
- package/interfaces/cli/_impl/commands/__init__.py +1 -0
- package/interfaces/cli/_impl/commands/agents.py +106 -0
- package/interfaces/cli/_impl/commands/architecture.py +63 -0
- package/interfaces/cli/_impl/commands/chat_repl.py +291 -0
- package/interfaces/cli/_impl/commands/common.py +17 -0
- package/interfaces/cli/_impl/commands/cron.py +145 -0
- package/interfaces/cli/_impl/commands/doctor.py +201 -0
- package/interfaces/cli/_impl/commands/gateway.py +111 -0
- package/interfaces/cli/_impl/commands/llm.py +113 -0
- package/interfaces/cli/_impl/commands/models.py +578 -0
- package/interfaces/cli/_impl/commands/orchestrator.py +71 -0
- package/interfaces/cli/_impl/commands/run_agent.py +102 -0
- package/interfaces/cli/_impl/commands/sessions.py +237 -0
- package/interfaces/cli/_impl/commands/skill.py +121 -0
- package/interfaces/cli/_impl/commands/smoke_test.py +193 -0
- package/interfaces/cli/_impl/commands/soul.py +106 -0
- package/interfaces/cli/_impl/commands/tools.py +249 -0
- package/interfaces/cli/_impl/handlers/__init__.py +1 -0
- package/interfaces/cli/_impl/main.py +727 -0
- package/interfaces/cli/_impl/parser/__init__.py +1 -0
- package/interfaces/cli/_impl/renderers/__init__.py +23 -0
- package/interfaces/cli/_impl/result.py +81 -0
- package/interfaces/gateway/__init__.py +1 -0
- package/interfaces/gateway/_impl/__init__.py +163 -0
- package/interfaces/gateway/_impl/adapters/dingtalk/__init__.py +8 -0
- package/interfaces/gateway/_impl/adapters/dingtalk/adapter.py +100 -0
- package/interfaces/gateway/_impl/adapters/feishu/__init__.py +8 -0
- package/interfaces/gateway/_impl/adapters/feishu/adapter.py +575 -0
- package/interfaces/gateway/_impl/adapters/qq/__init__.py +8 -0
- package/interfaces/gateway/_impl/adapters/qq/adapter.py +266 -0
- package/interfaces/gateway/_impl/adapters/slack/__init__.py +8 -0
- package/interfaces/gateway/_impl/adapters/slack/adapter.py +72 -0
- package/interfaces/gateway/_impl/adapters/wechat/__init__.py +8 -0
- package/interfaces/gateway/_impl/adapters/wechat/adapter.py +239 -0
- package/interfaces/gateway/_impl/middleware/__init__.py +1 -0
- package/interfaces/gateway/_impl/routes/__init__.py +1 -0
- package/interfaces/web/__init__.py +1 -0
- package/interfaces/web/frontend/.oxlintrc.json +8 -0
- package/interfaces/web/frontend/README.md +32 -0
- package/interfaces/web/frontend/index.html +13 -0
- package/interfaces/web/frontend/package-lock.json +5179 -0
- package/interfaces/web/frontend/package.json +48 -0
- package/interfaces/web/frontend/postcss.config.js +6 -0
- package/interfaces/web/frontend/public/cat-spritesheet.webp +0 -0
- package/interfaces/web/frontend/public/favicon.svg +1 -0
- package/interfaces/web/frontend/public/icons.svg +24 -0
- package/interfaces/web/frontend/run-5173.cmd +3 -0
- package/interfaces/web/frontend/src/App.css +184 -0
- package/interfaces/web/frontend/src/App.tsx +272 -0
- package/interfaces/web/frontend/src/api/client.ts +115 -0
- package/interfaces/web/frontend/src/assets/hero.png +0 -0
- package/interfaces/web/frontend/src/assets/react.svg +1 -0
- package/interfaces/web/frontend/src/assets/vite.svg +1 -0
- package/interfaces/web/frontend/src/components/AgentAvatar.tsx +59 -0
- package/interfaces/web/frontend/src/components/CatSpirit.tsx +248 -0
- package/interfaces/web/frontend/src/components/ChatFlow.tsx +587 -0
- package/interfaces/web/frontend/src/components/DateTimePicker.tsx +170 -0
- package/interfaces/web/frontend/src/components/MarkdownView.tsx +20 -0
- package/interfaces/web/frontend/src/components/NiceSelect.tsx +166 -0
- package/interfaces/web/frontend/src/components/PagePanel.tsx +68 -0
- package/interfaces/web/frontend/src/components/PixelCat.tsx +150 -0
- package/interfaces/web/frontend/src/components/RichEditor.tsx +129 -0
- package/interfaces/web/frontend/src/components/SakuraParticles.tsx +70 -0
- package/interfaces/web/frontend/src/components/SidebarLeft.tsx +100 -0
- package/interfaces/web/frontend/src/components/SidebarRight.tsx +333 -0
- package/interfaces/web/frontend/src/components/Toggle.tsx +19 -0
- package/interfaces/web/frontend/src/contexts/ThemeContext.tsx +230 -0
- package/interfaces/web/frontend/src/contexts/ThemeContextCore.ts +14 -0
- package/interfaces/web/frontend/src/contexts/useTheme.ts +8 -0
- package/interfaces/web/frontend/src/hooks/useAgent.ts +507 -0
- package/interfaces/web/frontend/src/hooks/useDashboardData.ts +160 -0
- package/interfaces/web/frontend/src/index.css +1903 -0
- package/interfaces/web/frontend/src/main.tsx +11 -0
- package/interfaces/web/frontend/src/mock/data.ts +3 -0
- package/interfaces/web/frontend/src/pages/AgentsPage.tsx +156 -0
- package/interfaces/web/frontend/src/pages/CronPage.tsx +80 -0
- package/interfaces/web/frontend/src/pages/FilesPage.tsx +57 -0
- package/interfaces/web/frontend/src/pages/McpPage.tsx +143 -0
- package/interfaces/web/frontend/src/pages/ModelsPage.tsx +170 -0
- package/interfaces/web/frontend/src/pages/SessionsPage.tsx +135 -0
- package/interfaces/web/frontend/src/pages/SkillsPage.tsx +4 -0
- package/interfaces/web/frontend/src/pages/SoulsPage.tsx +109 -0
- package/interfaces/web/frontend/src/pages/ToolsPage.tsx +3 -0
- package/interfaces/web/frontend/src/pages/UsagePage.tsx +132 -0
- package/interfaces/web/frontend/src/pages/WorkersPage.tsx +198 -0
- package/interfaces/web/frontend/src/types/index.ts +138 -0
- package/interfaces/web/frontend/tailwind.config.js +52 -0
- package/interfaces/web/frontend/tsconfig.app.json +26 -0
- package/interfaces/web/frontend/tsconfig.json +7 -0
- package/interfaces/web/frontend/tsconfig.node.json +23 -0
- package/interfaces/web/frontend/vite.config.ts +16 -0
- package/interfaces/web/frontend/yarn.lock +2281 -0
- package/interfaces/web/legacy_web/index.html +1259 -0
- package/lbs.py +6 -0
- package/package.json +53 -0
- package/requirements.txt +98 -0
- package/runtime/__init__.py +1 -0
- package/runtime/context/__init__.py +26 -0
- package/runtime/context/compression/strategies/__init__.py +1 -0
- package/runtime/context/compression/token_counter.py +62 -0
- package/runtime/context/compressor.py +594 -0
- package/runtime/context/management/__init__.py +1 -0
- package/runtime/context/management/context_builder.py +67 -0
- package/runtime/context/management/context_manager.py +1008 -0
- package/runtime/context/trimming/__init__.py +1 -0
- package/runtime/context/trimming/priority.py +58 -0
- package/runtime/context/trimming/window.py +56 -0
- package/runtime/core/__init__.py +1 -0
- package/runtime/core/llm/__init__.py +24 -0
- package/runtime/core/llm/auxiliary_client.py +279 -0
- package/runtime/core/llm/builtins.py +300 -0
- package/runtime/core/llm/client/__init__.py +699 -0
- package/runtime/core/llm/credential.py +334 -0
- package/runtime/core/llm/error_classifier.py +158 -0
- package/runtime/core/llm/factory/__init__.py +1 -0
- package/runtime/core/llm/health.py +106 -0
- package/runtime/core/llm/model_router.py +592 -0
- package/runtime/core/llm/provider.py +120 -0
- package/runtime/core/llm/redact.py +142 -0
- package/runtime/core/llm/registry/__init__.py +50 -0
- package/runtime/core/llm/registry/models.yaml +233 -0
- package/runtime/core/llm/think_scrubber.py +232 -0
- package/runtime/core/llm/tokenizer.py +150 -0
- package/runtime/core/llm/usage_pricing.py +373 -0
- package/runtime/core/sub_agents/__init__.py +11 -0
- package/runtime/core/sub_agents/agents.yaml +477 -0
- package/runtime/core/sub_agents/base.py +28 -0
- package/runtime/core/sub_agents/builtin/__init__.py +1 -0
- package/runtime/core/sub_agents/registry.py +96 -0
- package/runtime/core/tokenizer.py +146 -0
- package/runtime/gateway.py +14 -0
- package/runtime/memory/__init__.py +11 -0
- package/runtime/memory/context_memory.py +249 -0
- package/runtime/memory/long_term/knowledge_graph/__init__.py +1 -0
- package/runtime/memory/long_term/sqlite/__init__.py +104 -0
- package/runtime/memory/long_term/vector_store/__init__.py +1 -0
- package/runtime/memory/manager.py +144 -0
- package/runtime/memory/provider.py +97 -0
- package/runtime/memory/providers/long_term.py +143 -0
- package/runtime/memory/providers/working.py +53 -0
- package/runtime/memory/session/__init__.py +4 -0
- package/runtime/memory/session/identity.py +48 -0
- package/runtime/memory/session/session_context.py +23 -0
- package/runtime/memory/session/session_store.py +1118 -0
- package/runtime/memory/short_term/__init__.py +8 -0
- package/runtime/memory/short_term/buffer.py +8 -0
- package/runtime/memory/short_term/working.py +93 -0
- package/runtime/memory/task_contracts/__init__.py +115 -0
- package/runtime/orchestration/__init__.py +8 -0
- package/runtime/orchestration/auto_cycle/__init__.py +486 -0
- package/runtime/orchestration/auto_cycle/trigger.py +8 -0
- package/runtime/orchestration/background_review.py +470 -0
- package/runtime/orchestration/display.py +206 -0
- package/runtime/orchestration/iteration_budget.py +98 -0
- package/runtime/orchestration/loop/__init__.py +17 -0
- package/runtime/orchestration/loop/completion_gate.py +156 -0
- package/runtime/orchestration/loop/context_window.py +71 -0
- package/runtime/orchestration/loop/main_loop.py +2014 -0
- package/runtime/orchestration/loop/message_history.py +71 -0
- package/runtime/orchestration/loop/progress_tracker.py +211 -0
- package/runtime/orchestration/loop/run_trace.py +70 -0
- package/runtime/orchestration/loop/tool_call_executor.py +214 -0
- package/runtime/orchestration/loop/tool_execution_contract.py +134 -0
- package/runtime/orchestration/loop/types.py +282 -0
- package/runtime/orchestration/retry_engine.py +241 -0
- package/runtime/orchestration/runtime/__init__.py +20 -0
- package/runtime/orchestration/runtime/arbiter.py +85 -0
- package/runtime/orchestration/runtime/checkpoint.py +148 -0
- package/runtime/orchestration/runtime/error_policy.py +55 -0
- package/runtime/orchestration/runtime/events.py +92 -0
- package/runtime/orchestration/runtime/process_registry.py +605 -0
- package/runtime/orchestration/runtime/resume_manager.py +283 -0
- package/runtime/orchestration/runtime/retry_policy.py +26 -0
- package/runtime/orchestration/runtime/status.py +60 -0
- package/runtime/orchestration/runtime/types.py +74 -0
- package/runtime/orchestration/scheduler/__init__.py +8 -0
- package/runtime/orchestration/scheduler/master_scheduler.py +36 -0
- package/runtime/orchestration/scheduler/task_graph.py +67 -0
- package/runtime/orchestration/supervisor_loop.py +1041 -0
- package/runtime/orchestration/tool_guardrails.py +249 -0
- package/runtime/orchestration/tracer.py +403 -0
- package/runtime/orchestration/verification/__init__.py +14 -0
- package/runtime/orchestration/verification/completion.py +248 -0
- package/runtime/orchestration/verification/task_acceptance.py +87 -0
- package/runtime/orchestration/verification/tool_result.py +38 -0
- package/runtime/orchestration/verification/worker_result.py +54 -0
- package/runtime/perception/README.md +24 -0
- package/runtime/perception/__init__.py +5 -0
- package/runtime/perception/detectors/__init__.py +1 -0
- package/runtime/perception/detectors/entity.py +86 -0
- package/runtime/perception/detectors/intent.py +103 -0
- package/runtime/perception/input/multimodal/__init__.py +1 -0
- package/runtime/perception/input/streaming/__init__.py +1 -0
- package/runtime/perception/input/text/__init__.py +1 -0
- package/runtime/perception/preprocess/__init__.py +1 -0
- package/scripts/check_llm_layer.py +16 -0
- package/scripts/check_orchestration_layer.py +15 -0
- package/scripts/clean-pycache.js +56 -0
- package/scripts/init.js +187 -0
- package/tools/__init__.py +1 -0
- package/tools/mcp/clients/__init__.py +368 -0
- package/tools/mcp/registry.py +538 -0
- package/tools/mcp/servers/__init__.py +46 -0
- package/tools/skill/__init__.py +4 -0
- package/tools/skill/curator.py +368 -0
- package/tools/skill/registry/__init__.py +11 -0
- package/tools/skill/registry/registry.py +367 -0
- package/tools/skill/registry/source_config.py +101 -0
- package/tools/skill/skills/__init__.py +1 -0
- package/tools/tool/__init__.py +12 -0
- package/tools/tool/advanced_tools.py +307 -0
- package/tools/tool/approval_workflow.py +216 -0
- package/tools/tool/builtin.py +656 -0
- package/tools/tool/config.py +191 -0
- package/tools/tool/hermes_migrated.py +190 -0
- package/tools/tool/implementations/__init__.py +1 -0
- package/tools/tool/implementations/browser_use_provider.py +691 -0
- package/tools/tool/implementations/extended_tools.py +260 -0
- package/tools/tool/mcp_provider.py +95 -0
- package/tools/tool/policy.py +65 -0
- package/tools/tool/providers/base.py +115 -0
- package/tools/tool/registry/__init__.py +183 -0
- package/tools/tool/runtime.py +87 -0
- package/tools/tool/schema.py +99 -0
- package/tools/tool/skill_provider.py +164 -0
- package/tools/tool/todo_provider.py +161 -0
- package/tools/tool/tools.yaml +115 -0
|
@@ -0,0 +1,3136 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""OpenClaw -> Hermes migration helper.
|
|
3
|
+
|
|
4
|
+
This script migrates the parts of an OpenClaw user footprint that map cleanly
|
|
5
|
+
into Hermes Agent, archives selected unmapped docs for manual review, and
|
|
6
|
+
reports exactly what was skipped and why.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
from dataclasses import asdict, dataclass, field
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import yaml
|
|
24
|
+
except Exception: # pragma: no cover - 运行时处理
|
|
25
|
+
yaml = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ENTRY_DELIMITER = "\n§\n"
|
|
29
|
+
DEFAULT_MEMORY_CHAR_LIMIT = 2200
|
|
30
|
+
DEFAULT_USER_CHAR_LIMIT = 1375
|
|
31
|
+
SKILL_CATEGORY_DIRNAME = "openclaw-imports"
|
|
32
|
+
SKILL_CATEGORY_DESCRIPTION = (
|
|
33
|
+
"Skills migrated from an OpenClaw workspace."
|
|
34
|
+
)
|
|
35
|
+
SKILL_CONFLICT_MODES = {"skip", "overwrite", "rename"}
|
|
36
|
+
SUPPORTED_SECRET_TARGETS={
|
|
37
|
+
"TELEGRAM_BOT_TOKEN",
|
|
38
|
+
"OPENROUTER_API_KEY",
|
|
39
|
+
"OPENAI_API_KEY",
|
|
40
|
+
"ANTHROPIC_API_KEY",
|
|
41
|
+
"ELEVENLABS_API_KEY",
|
|
42
|
+
"VOICE_TOOLS_OPENAI_KEY",
|
|
43
|
+
}
|
|
44
|
+
WORKSPACE_INSTRUCTIONS_FILENAME = "AGENTS" + ".md"
|
|
45
|
+
MIGRATION_OPTION_METADATA: Dict[str, Dict[str, str]] = {
|
|
46
|
+
"soul": {
|
|
47
|
+
"label": "SOUL.md",
|
|
48
|
+
"description": "Import the OpenClaw persona file into Hermes.",
|
|
49
|
+
},
|
|
50
|
+
"workspace-agents": {
|
|
51
|
+
"label": "Workspace instructions",
|
|
52
|
+
"description": "Copy the OpenClaw workspace instructions file into a chosen workspace.",
|
|
53
|
+
},
|
|
54
|
+
"memory": {
|
|
55
|
+
"label": "MEMORY.md",
|
|
56
|
+
"description": "Import long-term memory entries into Hermes memories.",
|
|
57
|
+
},
|
|
58
|
+
"user-profile": {
|
|
59
|
+
"label": "USER.md",
|
|
60
|
+
"description": "Import user profile entries into Hermes memories.",
|
|
61
|
+
},
|
|
62
|
+
"messaging-settings": {
|
|
63
|
+
"label": "Messaging settings",
|
|
64
|
+
"description": "Import Hermes-compatible messaging settings such as allowlists and working directory.",
|
|
65
|
+
},
|
|
66
|
+
"secret-settings": {
|
|
67
|
+
"label": "Allowlisted secrets",
|
|
68
|
+
"description": "Import the small allowlist of Hermes-compatible secrets when explicitly enabled.",
|
|
69
|
+
},
|
|
70
|
+
"command-allowlist": {
|
|
71
|
+
"label": "Command allowlist",
|
|
72
|
+
"description": "Merge OpenClaw exec approval patterns into Hermes command_allowlist.",
|
|
73
|
+
},
|
|
74
|
+
"skills": {
|
|
75
|
+
"label": "User skills",
|
|
76
|
+
"description": "Copy OpenClaw skills into ~/.hermes/skills/openclaw-imports/.",
|
|
77
|
+
},
|
|
78
|
+
"tts-assets": {
|
|
79
|
+
"label": "TTS assets",
|
|
80
|
+
"description": "Copy compatible workspace TTS assets into ~/.hermes/tts/.",
|
|
81
|
+
},
|
|
82
|
+
"discord-settings": {
|
|
83
|
+
"label": "Discord settings",
|
|
84
|
+
"description": "Import Discord bot token and allowlist into Hermes .env.",
|
|
85
|
+
},
|
|
86
|
+
"slack-settings": {
|
|
87
|
+
"label": "Slack settings",
|
|
88
|
+
"description": "Import Slack bot/app tokens and allowlist into Hermes .env.",
|
|
89
|
+
},
|
|
90
|
+
"whatsapp-settings": {
|
|
91
|
+
"label": "WhatsApp settings",
|
|
92
|
+
"description": "Import WhatsApp allowlist into Hermes .env.",
|
|
93
|
+
},
|
|
94
|
+
"signal-settings": {
|
|
95
|
+
"label": "Signal settings",
|
|
96
|
+
"description": "Import Signal account, HTTP URL, and allowlist into Hermes .env.",
|
|
97
|
+
},
|
|
98
|
+
"provider-keys": {
|
|
99
|
+
"label": "Provider API keys",
|
|
100
|
+
"description": "Import model provider API keys into Hermes .env (requires --migrate-secrets).",
|
|
101
|
+
},
|
|
102
|
+
"model-config": {
|
|
103
|
+
"label": "Default model",
|
|
104
|
+
"description": "Import the default model setting into Hermes config.yaml.",
|
|
105
|
+
},
|
|
106
|
+
"tts-config": {
|
|
107
|
+
"label": "TTS configuration",
|
|
108
|
+
"description": "Import TTS provider and voice settings into Hermes config.yaml.",
|
|
109
|
+
},
|
|
110
|
+
"shared-skills": {
|
|
111
|
+
"label": "Shared skills",
|
|
112
|
+
"description": "Copy shared OpenClaw skills from ~/.openclaw/skills/ into Hermes.",
|
|
113
|
+
},
|
|
114
|
+
"daily-memory": {
|
|
115
|
+
"label": "Daily memory files",
|
|
116
|
+
"description": "Merge daily memory entries from workspace/memory/ into Hermes MEMORY.md.",
|
|
117
|
+
},
|
|
118
|
+
"archive": {
|
|
119
|
+
"label": "Archive unmapped docs",
|
|
120
|
+
"description": "Archive compatible-but-unmapped docs for later manual review.",
|
|
121
|
+
},
|
|
122
|
+
"mcp-servers": {
|
|
123
|
+
"label": "MCP servers",
|
|
124
|
+
"description": "Import MCP server definitions from OpenClaw into Hermes config.yaml.",
|
|
125
|
+
},
|
|
126
|
+
"plugins-config": {
|
|
127
|
+
"label": "Plugins configuration",
|
|
128
|
+
"description": "Archive OpenClaw plugin configuration and installed extensions for manual review.",
|
|
129
|
+
},
|
|
130
|
+
"cron-jobs": {
|
|
131
|
+
"label": "Cron / scheduled tasks",
|
|
132
|
+
"description": "Import cron job definitions. Archive for manual recreation via 'hermes cron'.",
|
|
133
|
+
},
|
|
134
|
+
"hooks-config": {
|
|
135
|
+
"label": "Hooks and webhooks",
|
|
136
|
+
"description": "Archive OpenClaw hook configuration (internal hooks, webhooks, Gmail integration).",
|
|
137
|
+
},
|
|
138
|
+
"agent-config": {
|
|
139
|
+
"label": "Agent defaults and multi-agent setup",
|
|
140
|
+
"description": "Import agent defaults (compaction, context, thinking) into Hermes config. Archive multi-agent list.",
|
|
141
|
+
},
|
|
142
|
+
"gateway-config": {
|
|
143
|
+
"label": "Gateway configuration",
|
|
144
|
+
"description": "Import gateway port and auth settings. Archive full gateway config for manual setup.",
|
|
145
|
+
},
|
|
146
|
+
"session-config": {
|
|
147
|
+
"label": "Session configuration",
|
|
148
|
+
"description": "Import session reset policies (daily/idle) into Hermes session_reset config.",
|
|
149
|
+
},
|
|
150
|
+
"full-providers": {
|
|
151
|
+
"label": "Full model provider definitions",
|
|
152
|
+
"description": "Import custom model providers (baseUrl, apiType, headers) into Hermes custom_providers.",
|
|
153
|
+
},
|
|
154
|
+
"deep-channels": {
|
|
155
|
+
"label": "Deep channel configuration",
|
|
156
|
+
"description": "Import extended channel settings (Matrix, Mattermost, IRC, group configs). Archive complex settings.",
|
|
157
|
+
},
|
|
158
|
+
"browser-config": {
|
|
159
|
+
"label": "Browser configuration",
|
|
160
|
+
"description": "Import browser automation settings into Hermes config.yaml.",
|
|
161
|
+
},
|
|
162
|
+
"tools-config": {
|
|
163
|
+
"label": "Tools configuration",
|
|
164
|
+
"description": "Import tool settings (exec timeout, sandbox, web search) into Hermes config.yaml.",
|
|
165
|
+
},
|
|
166
|
+
"approvals-config": {
|
|
167
|
+
"label": "Approval rules",
|
|
168
|
+
"description": "Import approval mode and rules into Hermes config.yaml approvals section.",
|
|
169
|
+
},
|
|
170
|
+
"memory-backend": {
|
|
171
|
+
"label": "Memory backend configuration",
|
|
172
|
+
"description": "Archive OpenClaw memory backend settings (QMD, vector search, citations) for manual review.",
|
|
173
|
+
},
|
|
174
|
+
"skills-config": {
|
|
175
|
+
"label": "Skills registry configuration",
|
|
176
|
+
"description": "Archive per-skill enabled/config/env settings from OpenClaw skills.entries.",
|
|
177
|
+
},
|
|
178
|
+
"ui-identity": {
|
|
179
|
+
"label": "UI and identity settings",
|
|
180
|
+
"description": "Archive OpenClaw UI theme, assistant identity, and display preferences.",
|
|
181
|
+
},
|
|
182
|
+
"logging-config": {
|
|
183
|
+
"label": "Logging and diagnostics",
|
|
184
|
+
"description": "Archive OpenClaw logging and diagnostics configuration.",
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
MIGRATION_PRESETS: Dict[str, set[str]] = {
|
|
188
|
+
"user-data": {
|
|
189
|
+
"soul",
|
|
190
|
+
"workspace-agents",
|
|
191
|
+
"memory",
|
|
192
|
+
"user-profile",
|
|
193
|
+
"messaging-settings",
|
|
194
|
+
"command-allowlist",
|
|
195
|
+
"skills",
|
|
196
|
+
"tts-assets",
|
|
197
|
+
"discord-settings",
|
|
198
|
+
"slack-settings",
|
|
199
|
+
"whatsapp-settings",
|
|
200
|
+
"signal-settings",
|
|
201
|
+
"model-config",
|
|
202
|
+
"tts-config",
|
|
203
|
+
"shared-skills",
|
|
204
|
+
"daily-memory",
|
|
205
|
+
"archive",
|
|
206
|
+
"mcp-servers",
|
|
207
|
+
"agent-config",
|
|
208
|
+
"session-config",
|
|
209
|
+
"browser-config",
|
|
210
|
+
"tools-config",
|
|
211
|
+
"approvals-config",
|
|
212
|
+
"deep-channels",
|
|
213
|
+
"full-providers",
|
|
214
|
+
"plugins-config",
|
|
215
|
+
"cron-jobs",
|
|
216
|
+
"hooks-config",
|
|
217
|
+
"memory-backend",
|
|
218
|
+
"skills-config",
|
|
219
|
+
"ui-identity",
|
|
220
|
+
"logging-config",
|
|
221
|
+
"gateway-config",
|
|
222
|
+
},
|
|
223
|
+
"full": set(MIGRATION_OPTION_METADATA),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
228
|
+
# Item 形状常量 — 为 report.json 的下游使用者保持稳定。
|
|
229
|
+
# 灵感来自 OpenClaw 的 src/plugin-sdk/migration.ts,以便双方使用相同的
|
|
230
|
+
# 词汇表。这些值特意与该脚本已经生成的字符串
|
|
231
|
+
# (migrated/archived/skipped/conflict/error)相匹配,因此该新增内容
|
|
232
|
+
# 是向后兼容的。
|
|
233
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
234
|
+
STATUS_MIGRATED = "migrated"
|
|
235
|
+
STATUS_ARCHIVED = "archived"
|
|
236
|
+
STATUS_SKIPPED = "skipped"
|
|
237
|
+
STATUS_CONFLICT = "conflict"
|
|
238
|
+
STATUS_ERROR = "error"
|
|
239
|
+
STATUS_PLANNED = "planned"
|
|
240
|
+
|
|
241
|
+
REASON_TARGET_EXISTS = "Target exists and overwrite is disabled"
|
|
242
|
+
REASON_BLOCKED_BY_APPLY_CONFLICT = "blocked by earlier apply conflict"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclass
|
|
246
|
+
class ItemResult:
|
|
247
|
+
kind: str
|
|
248
|
+
source: Optional[str]
|
|
249
|
+
destination: Optional[str]
|
|
250
|
+
status: str
|
|
251
|
+
reason: str = ""
|
|
252
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
|
253
|
+
sensitive: bool = False
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def parse_selection_values(values: Optional[Sequence[str]]) -> List[str]:
|
|
257
|
+
parsed: List[str] = []
|
|
258
|
+
for value in values or ():
|
|
259
|
+
for part in str(value).split(","):
|
|
260
|
+
part = part.strip().lower()
|
|
261
|
+
if part:
|
|
262
|
+
parsed.append(part)
|
|
263
|
+
return parsed
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def resolve_selected_options(
|
|
267
|
+
include: Optional[Sequence[str]] = None,
|
|
268
|
+
exclude: Optional[Sequence[str]] = None,
|
|
269
|
+
preset: Optional[str] = None,
|
|
270
|
+
) -> set[str]:
|
|
271
|
+
include_values = parse_selection_values(include)
|
|
272
|
+
exclude_values = parse_selection_values(exclude)
|
|
273
|
+
valid = set(MIGRATION_OPTION_METADATA)
|
|
274
|
+
preset_name = (preset or "").strip().lower()
|
|
275
|
+
|
|
276
|
+
if preset_name and preset_name not in MIGRATION_PRESETS:
|
|
277
|
+
raise ValueError(
|
|
278
|
+
"Unknown migration preset: "
|
|
279
|
+
+ preset_name
|
|
280
|
+
+ ". Valid presets: "
|
|
281
|
+
+ ", ".join(sorted(MIGRATION_PRESETS))
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
unknown = (set(include_values) - {"all"} - valid) | (set(exclude_values) - {"all"} - valid)
|
|
285
|
+
if unknown:
|
|
286
|
+
raise ValueError(
|
|
287
|
+
"Unknown migration option(s): "
|
|
288
|
+
+ ", ".join(sorted(unknown))
|
|
289
|
+
+ ". Valid options: "
|
|
290
|
+
+ ", ".join(sorted(valid))
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if preset_name:
|
|
294
|
+
selected = set(MIGRATION_PRESETS[preset_name])
|
|
295
|
+
elif not include_values or "all" in include_values:
|
|
296
|
+
selected = set(valid)
|
|
297
|
+
else:
|
|
298
|
+
selected = set(include_values)
|
|
299
|
+
|
|
300
|
+
if "all" in exclude_values:
|
|
301
|
+
selected.clear()
|
|
302
|
+
selected -= (set(exclude_values) - {"all"})
|
|
303
|
+
return selected
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def sha256_file(path: Path) -> str:
|
|
307
|
+
h = hashlib.sha256()
|
|
308
|
+
with path.open("rb") as fh:
|
|
309
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
310
|
+
h.update(chunk)
|
|
311
|
+
return h.hexdigest()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def read_text(path: Path) -> str:
|
|
315
|
+
return path.read_text(encoding="utf-8")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def normalize_text(text: str) -> str:
|
|
319
|
+
return re.sub(r"\s+", " ", text.strip())
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def ensure_parent(path: Path) -> None:
|
|
323
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def resolve_secret_input(value: Any, env: Optional[Dict[str, str]] = None) -> Optional[str]:
|
|
327
|
+
"""Resolve an OpenClaw SecretInput value to a plain string.
|
|
328
|
+
|
|
329
|
+
SecretInput can be:
|
|
330
|
+
- A plain string: "sk-..."
|
|
331
|
+
- An env template: "${OPENROUTER_API_KEY}"
|
|
332
|
+
- A SecretRef object: {"source": "env", "id": "OPENROUTER_API_KEY"}
|
|
333
|
+
"""
|
|
334
|
+
if isinstance(value, str):
|
|
335
|
+
# 检查环境变量模板:"${VAR_NAME}"
|
|
336
|
+
m = re.match(r"^\$\{(\w+)\}$", value.strip())
|
|
337
|
+
if m and env:
|
|
338
|
+
return env.get(m.group(1), "").strip() or None
|
|
339
|
+
return value.strip() or None
|
|
340
|
+
if isinstance(value, dict):
|
|
341
|
+
source = value.get("source", "")
|
|
342
|
+
ref_id = value.get("id", "")
|
|
343
|
+
if source == "env" and ref_id and env:
|
|
344
|
+
return env.get(ref_id, "").strip() or None
|
|
345
|
+
# 无法在此处解析文件/可执行文件来源 — 返回 None
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def load_yaml_file(path: Path) -> Dict[str, Any]:
|
|
350
|
+
if yaml is None or not path.exists():
|
|
351
|
+
return {}
|
|
352
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
353
|
+
return data if isinstance(data, dict) else {}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def dump_yaml_file(path: Path, data: Dict[str, Any]) -> None:
|
|
357
|
+
if yaml is None:
|
|
358
|
+
raise RuntimeError("PyYAML is required to update Hermes config.yaml")
|
|
359
|
+
ensure_parent(path)
|
|
360
|
+
path.write_text(
|
|
361
|
+
yaml.safe_dump(data, sort_keys=False, allow_unicode=False),
|
|
362
|
+
encoding="utf-8",
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def parse_env_file(path: Path) -> Dict[str, str]:
|
|
367
|
+
if not path.exists():
|
|
368
|
+
return {}
|
|
369
|
+
data: Dict[str, str] = {}
|
|
370
|
+
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
371
|
+
line = raw_line.strip()
|
|
372
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
373
|
+
continue
|
|
374
|
+
key, _, value = line.partition("=")
|
|
375
|
+
data[key.strip()] = value.strip()
|
|
376
|
+
return data
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def save_env_file(path: Path, data: Dict[str, str]) -> None:
|
|
380
|
+
ensure_parent(path)
|
|
381
|
+
lines = [f"{key}={value}" for key, value in data.items()]
|
|
382
|
+
path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def backup_existing(path: Path, backup_root: Path) -> Optional[Path]:
|
|
386
|
+
if not path.exists():
|
|
387
|
+
return None
|
|
388
|
+
rel = Path(*path.parts[1:]) if path.is_absolute() and len(path.parts) > 1 else path
|
|
389
|
+
dest = backup_root / rel
|
|
390
|
+
ensure_parent(dest)
|
|
391
|
+
if path.is_dir():
|
|
392
|
+
shutil.copytree(path, dest, dirs_exist_ok=True)
|
|
393
|
+
else:
|
|
394
|
+
shutil.copy2(path, dest)
|
|
395
|
+
return dest
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ── 品牌重写 ─────────────────────────────────────────────────
|
|
399
|
+
# 在迁移后的文本中将 OpenClaw 品牌名称替换为 Hermes,以便
|
|
400
|
+
# 记忆条目、用户配置文件、SOUL.md 和工作区指令
|
|
401
|
+
# 读起来像是在自指新的智能体身份。
|
|
402
|
+
#
|
|
403
|
+
# 保留大小写:``OpenClaw`` → ``Hermes`` (正文),但小写匹配
|
|
404
|
+
# 如 ``openclaw`` → ``hermes`` (这样像 ``~/.openclaw`` 这样的文件系统路径
|
|
405
|
+
# 会变成 ``~/.hermes`` ——真正的 Hermes 主目录——而不是错误的 ``~/.Hermes``)。
|
|
406
|
+
_REBRAND_PATTERNS: List[Tuple[re.Pattern, str]] = [
|
|
407
|
+
(re.compile(r'\bOpen[\s-]?Claw\b', re.IGNORECASE), 'Hermes'),
|
|
408
|
+
(re.compile(r'\bClawdBot\b', re.IGNORECASE), 'Hermes'),
|
|
409
|
+
(re.compile(r'\bMoltBot\b', re.IGNORECASE), 'Hermes'),
|
|
410
|
+
]
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _case_preserving_replacement(replacement: str):
|
|
414
|
+
"""Return a re.sub replacement fn that lowercases the result when the
|
|
415
|
+
matched text was all-lowercase.
|
|
416
|
+
|
|
417
|
+
Keeps ``OpenClaw`` → ``Hermes`` but maps ``openclaw`` → ``hermes`` so a
|
|
418
|
+
filesystem path like ``~/.openclaw/config.yaml`` rewrites to
|
|
419
|
+
``~/.hermes/config.yaml`` (the real Hermes home) instead of the broken
|
|
420
|
+
``~/.Hermes/config.yaml``.
|
|
421
|
+
"""
|
|
422
|
+
def _sub(match: "re.Match[str]") -> str:
|
|
423
|
+
matched = match.group(0)
|
|
424
|
+
if matched and matched.islower():
|
|
425
|
+
return replacement.lower()
|
|
426
|
+
return replacement
|
|
427
|
+
return _sub
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def rebrand_text(text: str) -> str:
|
|
431
|
+
"""Replace OpenClaw / ClawdBot / MoltBot brand names with Hermes.
|
|
432
|
+
|
|
433
|
+
Preserves case so filesystem-path matches (lowercase) don't become
|
|
434
|
+
capitalized directory names that don't exist.
|
|
435
|
+
"""
|
|
436
|
+
for pattern, replacement in _REBRAND_PATTERNS:
|
|
437
|
+
text = pattern.sub(_case_preserving_replacement(replacement), text)
|
|
438
|
+
return text
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def parse_existing_memory_entries(path: Path) -> List[str]:
|
|
442
|
+
if not path.exists():
|
|
443
|
+
return []
|
|
444
|
+
raw = read_text(path)
|
|
445
|
+
if not raw.strip():
|
|
446
|
+
return []
|
|
447
|
+
if ENTRY_DELIMITER in raw:
|
|
448
|
+
return [e.strip() for e in raw.split(ENTRY_DELIMITER) if e.strip()]
|
|
449
|
+
return extract_markdown_entries(raw)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def extract_markdown_entries(text: str) -> List[str]:
|
|
453
|
+
entries: List[str] = []
|
|
454
|
+
headings: List[str] = []
|
|
455
|
+
paragraph_lines: List[str] = []
|
|
456
|
+
|
|
457
|
+
def context_prefix() -> str:
|
|
458
|
+
filtered = [h for h in headings if h and not re.search(r"\b(MEMORY|USER|SOUL|AGENTS|TOOLS|IDENTITY)\.md\b", h, re.I)]
|
|
459
|
+
return " > ".join(filtered)
|
|
460
|
+
|
|
461
|
+
def flush_paragraph() -> None:
|
|
462
|
+
nonlocal paragraph_lines
|
|
463
|
+
if not paragraph_lines:
|
|
464
|
+
return
|
|
465
|
+
text_block = " ".join(line.strip() for line in paragraph_lines).strip()
|
|
466
|
+
paragraph_lines = []
|
|
467
|
+
if not text_block:
|
|
468
|
+
return
|
|
469
|
+
prefix = context_prefix()
|
|
470
|
+
if prefix:
|
|
471
|
+
entries.append(f"{prefix}: {text_block}")
|
|
472
|
+
else:
|
|
473
|
+
entries.append(text_block)
|
|
474
|
+
|
|
475
|
+
in_code_block = False
|
|
476
|
+
for raw_line in text.splitlines():
|
|
477
|
+
line = raw_line.rstrip()
|
|
478
|
+
stripped = line.strip()
|
|
479
|
+
|
|
480
|
+
if stripped.startswith("```"):
|
|
481
|
+
in_code_block = not in_code_block
|
|
482
|
+
flush_paragraph()
|
|
483
|
+
continue
|
|
484
|
+
if in_code_block:
|
|
485
|
+
continue
|
|
486
|
+
|
|
487
|
+
heading_match = re.match(r"^(#{1,6})\s+(.*\S)\s*$", stripped)
|
|
488
|
+
if heading_match:
|
|
489
|
+
flush_paragraph()
|
|
490
|
+
level = len(heading_match.group(1))
|
|
491
|
+
text_value = heading_match.group(2).strip()
|
|
492
|
+
while len(headings) >= level:
|
|
493
|
+
headings.pop()
|
|
494
|
+
headings.append(text_value)
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
bullet_match = re.match(r"^\s*(?:[-*]|\d+\.)\s+(.*\S)\s*$", line)
|
|
498
|
+
if bullet_match:
|
|
499
|
+
flush_paragraph()
|
|
500
|
+
content = bullet_match.group(1).strip()
|
|
501
|
+
prefix = context_prefix()
|
|
502
|
+
entries.append(f"{prefix}: {content}" if prefix else content)
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
if not stripped:
|
|
506
|
+
flush_paragraph()
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
if stripped.startswith("|") and stripped.endswith("|"):
|
|
510
|
+
flush_paragraph()
|
|
511
|
+
continue
|
|
512
|
+
|
|
513
|
+
paragraph_lines.append(stripped)
|
|
514
|
+
|
|
515
|
+
flush_paragraph()
|
|
516
|
+
|
|
517
|
+
deduped: List[str] = []
|
|
518
|
+
seen = set()
|
|
519
|
+
for entry in entries:
|
|
520
|
+
normalized = normalize_text(entry)
|
|
521
|
+
if not normalized or normalized in seen:
|
|
522
|
+
continue
|
|
523
|
+
seen.add(normalized)
|
|
524
|
+
deduped.append(entry.strip())
|
|
525
|
+
return deduped
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def merge_entries(
|
|
529
|
+
existing: Sequence[str],
|
|
530
|
+
incoming: Sequence[str],
|
|
531
|
+
limit: int,
|
|
532
|
+
) -> Tuple[List[str], Dict[str, int], List[str]]:
|
|
533
|
+
merged = list(existing)
|
|
534
|
+
seen = {normalize_text(entry) for entry in existing if entry.strip()}
|
|
535
|
+
stats = {"existing": len(existing), "added": 0, "duplicates": 0, "overflowed": 0}
|
|
536
|
+
overflowed: List[str] = []
|
|
537
|
+
|
|
538
|
+
current_len = len(ENTRY_DELIMITER.join(merged)) if merged else 0
|
|
539
|
+
|
|
540
|
+
for entry in incoming:
|
|
541
|
+
normalized = normalize_text(entry)
|
|
542
|
+
if not normalized:
|
|
543
|
+
continue
|
|
544
|
+
if normalized in seen:
|
|
545
|
+
stats["duplicates"] += 1
|
|
546
|
+
continue
|
|
547
|
+
|
|
548
|
+
candidate_len = len(entry) if not merged else current_len + len(ENTRY_DELIMITER) + len(entry)
|
|
549
|
+
if candidate_len > limit:
|
|
550
|
+
stats["overflowed"] += 1
|
|
551
|
+
overflowed.append(entry)
|
|
552
|
+
continue
|
|
553
|
+
|
|
554
|
+
merged.append(entry)
|
|
555
|
+
seen.add(normalized)
|
|
556
|
+
current_len = candidate_len
|
|
557
|
+
stats["added"] += 1
|
|
558
|
+
|
|
559
|
+
return merged, stats, overflowed
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def relative_label(path: Path, root: Path) -> str:
|
|
563
|
+
try:
|
|
564
|
+
return str(path.relative_to(root))
|
|
565
|
+
except ValueError:
|
|
566
|
+
return str(path)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
570
|
+
# 迁移报告的密钥脱敏。
|
|
571
|
+
#
|
|
572
|
+
# 报告 JSON 会持久化到磁盘上的迁移输出目录中,并且
|
|
573
|
+
# 经常出现在错误报告或支持渠道中。任何看起来像
|
|
574
|
+
# 凭据的内容——无论是根据键名还是值形状——在写入报告之前,
|
|
575
|
+
# 都会被替换为 "[redacted]"。
|
|
576
|
+
#
|
|
577
|
+
# 参考 OpenClaw 的 src/plugin-sdk/migration.ts,以便两种迁移工具
|
|
578
|
+
# 脱敏方式保持一致。纯函数——对任何纯数据字典调用都很安全。
|
|
579
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
580
|
+
REDACTED_MIGRATION_VALUE = "[redacted]"
|
|
581
|
+
|
|
582
|
+
_SECRET_KEY_MARKERS = (
|
|
583
|
+
"accesstoken",
|
|
584
|
+
"apikey",
|
|
585
|
+
"authorization",
|
|
586
|
+
"bearertoken",
|
|
587
|
+
"clientsecret",
|
|
588
|
+
"cookie",
|
|
589
|
+
"credential",
|
|
590
|
+
"password",
|
|
591
|
+
"privatekey",
|
|
592
|
+
"refreshtoken",
|
|
593
|
+
"secret",
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
_SECRET_VALUE_PATTERNS = (
|
|
597
|
+
re.compile(r"\bBearer\s+[A-Za-z0-9._~+/=\-]+"),
|
|
598
|
+
re.compile(r"\bsk-[A-Za-z0-9_\-]{8,}\b"),
|
|
599
|
+
re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{16,}\b"),
|
|
600
|
+
re.compile(r"\bxox[abprs]-[A-Za-z0-9\-]{8,}\b"),
|
|
601
|
+
re.compile(r"\bAIza[0-9A-Za-z_\-]{12,}\b"),
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _normalize_secret_key(key: str) -> str:
|
|
606
|
+
return re.sub(r"[^a-z0-9]", "", key.lower())
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _is_secret_key(key: str) -> bool:
|
|
610
|
+
normalized = _normalize_secret_key(key)
|
|
611
|
+
if normalized == "token" or normalized.endswith("token"):
|
|
612
|
+
return True
|
|
613
|
+
if normalized in {"auth", "authorization"}:
|
|
614
|
+
return True
|
|
615
|
+
return any(marker in normalized for marker in _SECRET_KEY_MARKERS)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _redact_string(value: str) -> str:
|
|
619
|
+
for pattern in _SECRET_VALUE_PATTERNS:
|
|
620
|
+
value = pattern.sub(REDACTED_MIGRATION_VALUE, value)
|
|
621
|
+
return value
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def redact_migration_value(value: Any) -> Any:
|
|
625
|
+
"""Return a deep copy of ``value`` with secret-looking content replaced.
|
|
626
|
+
|
|
627
|
+
Applied to every report written to disk. Keys whose normalized form
|
|
628
|
+
matches a credential marker get their value replaced wholesale. Strings
|
|
629
|
+
anywhere in the tree are scanned for common token patterns (sk-..., ghp_...,
|
|
630
|
+
xox*-, AIza*, Bearer ...) and those substrings are replaced inline.
|
|
631
|
+
"""
|
|
632
|
+
return _redact_internal(value, set())
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _redact_internal(value: Any, seen: set) -> Any:
|
|
636
|
+
if isinstance(value, str):
|
|
637
|
+
return _redact_string(value)
|
|
638
|
+
if isinstance(value, (list, tuple)):
|
|
639
|
+
return [_redact_internal(entry, seen) for entry in value]
|
|
640
|
+
if isinstance(value, dict):
|
|
641
|
+
obj_id = id(value)
|
|
642
|
+
if obj_id in seen:
|
|
643
|
+
return REDACTED_MIGRATION_VALUE
|
|
644
|
+
seen.add(obj_id)
|
|
645
|
+
out: Dict[str, Any] = {}
|
|
646
|
+
for key, entry in value.items():
|
|
647
|
+
if isinstance(key, str) and _is_secret_key(key):
|
|
648
|
+
out[key] = REDACTED_MIGRATION_VALUE
|
|
649
|
+
else:
|
|
650
|
+
out[key] = _redact_internal(entry, seen)
|
|
651
|
+
return out
|
|
652
|
+
return value
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def write_report(output_dir: Path, report: Dict[str, Any]) -> None:
|
|
656
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
657
|
+
# 持久化前务必脱敏。需要原始对象
|
|
658
|
+
# (进程内)的调用者仍然可以从 build_report() 获取它;只有磁盘上的
|
|
659
|
+
# 副本会被脱敏。
|
|
660
|
+
redacted = redact_migration_value(report)
|
|
661
|
+
(output_dir / "report.json").write_text(
|
|
662
|
+
json.dumps(redacted, indent=2, ensure_ascii=False) + "\n",
|
|
663
|
+
encoding="utf-8",
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
grouped: Dict[str, List[Dict[str, Any]]] = {}
|
|
667
|
+
for item in redacted["items"]:
|
|
668
|
+
grouped.setdefault(item["status"], []).append(item)
|
|
669
|
+
|
|
670
|
+
lines = [
|
|
671
|
+
"# OpenClaw -> Hermes Migration Report",
|
|
672
|
+
"",
|
|
673
|
+
f"- Timestamp: {redacted['timestamp']}",
|
|
674
|
+
f"- Mode: {redacted['mode']}",
|
|
675
|
+
f"- Source: `{redacted['source_root']}`",
|
|
676
|
+
f"- Target: `{redacted['target_root']}`",
|
|
677
|
+
"",
|
|
678
|
+
"## Summary",
|
|
679
|
+
"",
|
|
680
|
+
]
|
|
681
|
+
|
|
682
|
+
for key, value in redacted["summary"].items():
|
|
683
|
+
lines.append(f"- {key}: {value}")
|
|
684
|
+
|
|
685
|
+
warnings = redacted.get("warnings") or []
|
|
686
|
+
if warnings:
|
|
687
|
+
lines.extend(["", "## Warnings", ""])
|
|
688
|
+
for warning in warnings:
|
|
689
|
+
lines.append(f"- {warning}")
|
|
690
|
+
|
|
691
|
+
lines.extend(["", "## What Was Not Fully Brought Over", ""])
|
|
692
|
+
skipped = grouped.get("skipped", []) + grouped.get("conflict", []) + grouped.get("error", [])
|
|
693
|
+
if not skipped:
|
|
694
|
+
lines.append("- Nothing. All discovered items were either migrated or archived.")
|
|
695
|
+
else:
|
|
696
|
+
for item in skipped:
|
|
697
|
+
source = item["source"] or "(n/a)"
|
|
698
|
+
dest = item["destination"] or "(n/a)"
|
|
699
|
+
reason = item["reason"] or item["status"]
|
|
700
|
+
lines.append(f"- `{source}` -> `{dest}`: {reason}")
|
|
701
|
+
|
|
702
|
+
next_steps = redacted.get("next_steps") or []
|
|
703
|
+
if next_steps:
|
|
704
|
+
lines.extend(["", "## Next Steps", ""])
|
|
705
|
+
for step in next_steps:
|
|
706
|
+
lines.append(f"- {step}")
|
|
707
|
+
|
|
708
|
+
(output_dir / "summary.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
class Migrator:
|
|
712
|
+
def __init__(
|
|
713
|
+
self,
|
|
714
|
+
source_root: Path,
|
|
715
|
+
target_root: Path,
|
|
716
|
+
execute: bool,
|
|
717
|
+
workspace_target: Optional[Path],
|
|
718
|
+
overwrite: bool,
|
|
719
|
+
migrate_secrets: bool,
|
|
720
|
+
output_dir: Optional[Path],
|
|
721
|
+
selected_options: Optional[set[str]] = None,
|
|
722
|
+
preset_name: str = "",
|
|
723
|
+
skill_conflict_mode: str = "skip",
|
|
724
|
+
):
|
|
725
|
+
self.source_root = source_root
|
|
726
|
+
self.target_root = target_root
|
|
727
|
+
self.execute = execute
|
|
728
|
+
self.workspace_target = workspace_target
|
|
729
|
+
self.overwrite = overwrite
|
|
730
|
+
self.migrate_secrets = migrate_secrets
|
|
731
|
+
self.selected_options = set(selected_options or MIGRATION_OPTION_METADATA.keys())
|
|
732
|
+
self.preset_name = preset_name.strip().lower()
|
|
733
|
+
self.skill_conflict_mode = skill_conflict_mode.strip().lower() or "skip"
|
|
734
|
+
self.timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
|
|
735
|
+
self.output_dir = output_dir or (
|
|
736
|
+
target_root / "migration" / "openclaw" / self.timestamp if execute else None
|
|
737
|
+
)
|
|
738
|
+
self.archive_dir = self.output_dir / "archive" if self.output_dir else None
|
|
739
|
+
self.backup_dir = self.output_dir / "backups" if self.output_dir else None
|
|
740
|
+
self.overflow_dir = self.output_dir / "overflow" if self.output_dir else None
|
|
741
|
+
self.items: List[ItemResult] = []
|
|
742
|
+
# 一旦 config.yaml 的写入在运行中遇到冲突/错误,后续的
|
|
743
|
+
# config.yaml 写入将被有意短路,以避免
|
|
744
|
+
# 导致配置停留在半写入状态。参考
|
|
745
|
+
# OpenClaw 的 extensions/migrate-hermes/apply.ts 中 "被先前的
|
|
746
|
+
# 应用冲突阻止" 的处理顺序。
|
|
747
|
+
self._config_apply_blocked: bool = False
|
|
748
|
+
|
|
749
|
+
# 从 openclaw.json 解析配置的工作区目录。
|
|
750
|
+
# 许多用户(特别是那些在 OpenClaw 品牌重塑之前就开始使用的用户)
|
|
751
|
+
# 拥有与默认 ~/.openclaw/workspace/ 不同的自定义工作区路径
|
|
752
|
+
# (例如 ~/clawd/)。读取 agents.defaults.workspace
|
|
753
|
+
# 可以让 source_candidate() 在实际工作区中查找文件。
|
|
754
|
+
self._custom_workspace: Optional[Path] = None
|
|
755
|
+
oc_config = self.load_openclaw_config()
|
|
756
|
+
ws = (oc_config.get("agents", {}).get("defaults", {}).get("workspace") or "").strip()
|
|
757
|
+
if ws:
|
|
758
|
+
ws_path = Path(ws).expanduser().resolve()
|
|
759
|
+
# 仅当它存在且位于 source_root 树之外时才使用它
|
|
760
|
+
# (otherwise the standard relative-path logic already covers it).
|
|
761
|
+
if ws_path.is_dir():
|
|
762
|
+
try:
|
|
763
|
+
ws_path.relative_to(self.source_root)
|
|
764
|
+
except ValueError:
|
|
765
|
+
# ws_path 在 source_root 之外 —— 用作自定义 workspace
|
|
766
|
+
self._custom_workspace = ws_path
|
|
767
|
+
|
|
768
|
+
config = load_yaml_file(self.target_root / "config.yaml")
|
|
769
|
+
mem_cfg = config.get("memory", {}) if isinstance(config.get("memory"), dict) else {}
|
|
770
|
+
self.memory_limit = int(mem_cfg.get("memory_char_limit", DEFAULT_MEMORY_CHAR_LIMIT))
|
|
771
|
+
self.user_limit = int(mem_cfg.get("user_char_limit", DEFAULT_USER_CHAR_LIMIT))
|
|
772
|
+
|
|
773
|
+
if self.skill_conflict_mode not in SKILL_CONFLICT_MODES:
|
|
774
|
+
raise ValueError(
|
|
775
|
+
"Unknown skill conflict mode: "
|
|
776
|
+
+ self.skill_conflict_mode
|
|
777
|
+
+ ". Valid modes: "
|
|
778
|
+
+ ", ".join(sorted(SKILL_CONFLICT_MODES))
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
def is_selected(self, option_id: str) -> bool:
|
|
782
|
+
return option_id in self.selected_options
|
|
783
|
+
|
|
784
|
+
# 修改 Hermes config.yaml 文件的选项 id。
|
|
785
|
+
# 一旦任何一个在 config.yaml 上记录了冲突/错误,
|
|
786
|
+
# 后续的短路以避免部分写入。
|
|
787
|
+
# 与调用 load_yaml_file + dump_yaml_file 的方法保持同步。
|
|
788
|
+
_CONFIG_MUTATING_OPTIONS = frozenset({
|
|
789
|
+
"model-config",
|
|
790
|
+
"tts-config",
|
|
791
|
+
"mcp-servers",
|
|
792
|
+
"plugins-config",
|
|
793
|
+
"cron-jobs",
|
|
794
|
+
"hooks-config",
|
|
795
|
+
"agent-config",
|
|
796
|
+
"gateway-config",
|
|
797
|
+
"session-config",
|
|
798
|
+
"full-providers",
|
|
799
|
+
"deep-channels",
|
|
800
|
+
"browser-config",
|
|
801
|
+
"tools-config",
|
|
802
|
+
"approvals-config",
|
|
803
|
+
"memory-backend",
|
|
804
|
+
"skills-config",
|
|
805
|
+
"ui-identity",
|
|
806
|
+
"logging-config",
|
|
807
|
+
"command-allowlist",
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
def record(
|
|
811
|
+
self,
|
|
812
|
+
kind: str,
|
|
813
|
+
source: Optional[Path],
|
|
814
|
+
destination: Optional[Path],
|
|
815
|
+
status: str,
|
|
816
|
+
reason: str = "",
|
|
817
|
+
**details: Any,
|
|
818
|
+
) -> None:
|
|
819
|
+
sensitive = bool(details.pop("sensitive", False))
|
|
820
|
+
self.items.append(
|
|
821
|
+
ItemResult(
|
|
822
|
+
kind=kind,
|
|
823
|
+
source=str(source) if source else None,
|
|
824
|
+
destination=str(destination) if destination else None,
|
|
825
|
+
status=status,
|
|
826
|
+
reason=reason,
|
|
827
|
+
details=details,
|
|
828
|
+
sensitive=sensitive,
|
|
829
|
+
)
|
|
830
|
+
)
|
|
831
|
+
# 当 config.yaml 写入发生冲突/错误时翻转配置块标志。
|
|
832
|
+
# 后续配置修改选项将跳过而非
|
|
833
|
+
# 尝试部分写入。
|
|
834
|
+
if status in {STATUS_CONFLICT, STATUS_ERROR} and destination is not None:
|
|
835
|
+
dest_str = str(destination)
|
|
836
|
+
if dest_str.endswith("config.yaml") or dest_str.endswith("config.yml"):
|
|
837
|
+
self._config_apply_blocked = True
|
|
838
|
+
|
|
839
|
+
def source_candidate(self, *relative_paths: str) -> Optional[Path]:
|
|
840
|
+
for rel in relative_paths:
|
|
841
|
+
candidate = self.source_root / rel
|
|
842
|
+
if candidate.exists():
|
|
843
|
+
return candidate
|
|
844
|
+
# OpenClaw 将 workspace/ 重命名为 workspace-main/
|
|
845
|
+
# (多 agent 时为 workspace-{agentId})。
|
|
846
|
+
if rel.startswith("workspace/"):
|
|
847
|
+
suffix = rel[len("workspace/"):]
|
|
848
|
+
for variant in ("workspace-main", "workspace-assistant"):
|
|
849
|
+
alt = self.source_root / variant / suffix
|
|
850
|
+
if alt.exists():
|
|
851
|
+
return alt
|
|
852
|
+
elif rel.startswith("workspace.default/"):
|
|
853
|
+
suffix = rel[len("workspace.default/"):]
|
|
854
|
+
alt = self.source_root / "workspace-main" / suffix
|
|
855
|
+
if alt.exists():
|
|
856
|
+
return alt
|
|
857
|
+
|
|
858
|
+
# 尝试新路径作为回退。
|
|
859
|
+
# 最终回退:从 openclaw.json 的
|
|
860
|
+
# agents.defaults.workspace 检查配置的 workspace 目录。
|
|
861
|
+
# 在 OpenClaw 品牌重塑前开始的用户
|
|
862
|
+
if self._custom_workspace:
|
|
863
|
+
for rel in relative_paths:
|
|
864
|
+
# 通常有 ~/.openclaw/ 之外的自定义 workspace 路径。
|
|
865
|
+
# 剥离前导"workspace/"或"workspace.default/"
|
|
866
|
+
for prefix in ("workspace/", "workspace.default/"):
|
|
867
|
+
if rel.startswith(prefix):
|
|
868
|
+
suffix = rel[len(prefix):]
|
|
869
|
+
alt = self._custom_workspace / suffix
|
|
870
|
+
if alt.exists():
|
|
871
|
+
return alt
|
|
872
|
+
break
|
|
873
|
+
|
|
874
|
+
return None
|
|
875
|
+
|
|
876
|
+
def resolve_skill_destination(self, destination: Path) -> Path:
|
|
877
|
+
if self.skill_conflict_mode != "rename" or not destination.exists():
|
|
878
|
+
return destination
|
|
879
|
+
|
|
880
|
+
suffix = "-imported"
|
|
881
|
+
candidate = destination.with_name(destination.name + suffix)
|
|
882
|
+
counter = 2
|
|
883
|
+
while candidate.exists():
|
|
884
|
+
candidate = destination.with_name(f"{destination.name}{suffix}-{counter}")
|
|
885
|
+
counter += 1
|
|
886
|
+
return candidate
|
|
887
|
+
|
|
888
|
+
def migrate(self) -> Dict[str, Any]:
|
|
889
|
+
if not self.source_root.exists():
|
|
890
|
+
self.record("source", self.source_root, None, "error", "OpenClaw directory does not exist")
|
|
891
|
+
return self.build_report()
|
|
892
|
+
|
|
893
|
+
config = self.load_openclaw_config()
|
|
894
|
+
|
|
895
|
+
self.run_if_selected("soul", self.migrate_soul)
|
|
896
|
+
self.run_if_selected("workspace-agents", self.migrate_workspace_agents)
|
|
897
|
+
self.run_if_selected(
|
|
898
|
+
"memory",
|
|
899
|
+
lambda: self.migrate_memory(
|
|
900
|
+
self.source_candidate("workspace/MEMORY.md", "workspace.default/MEMORY.md"),
|
|
901
|
+
self.target_root / "memories" / "MEMORY.md",
|
|
902
|
+
self.memory_limit,
|
|
903
|
+
kind="memory",
|
|
904
|
+
),
|
|
905
|
+
)
|
|
906
|
+
self.run_if_selected(
|
|
907
|
+
"user-profile",
|
|
908
|
+
lambda: self.migrate_memory(
|
|
909
|
+
self.source_candidate("workspace/USER.md", "workspace.default/USER.md"),
|
|
910
|
+
self.target_root / "memories" / "USER.md",
|
|
911
|
+
self.user_limit,
|
|
912
|
+
kind="user-profile",
|
|
913
|
+
),
|
|
914
|
+
)
|
|
915
|
+
self.run_if_selected("messaging-settings", lambda: self.migrate_messaging_settings(config))
|
|
916
|
+
self.run_if_selected("secret-settings", lambda: self.handle_secret_settings(config))
|
|
917
|
+
self.run_if_selected("discord-settings", lambda: self.migrate_discord_settings(config))
|
|
918
|
+
self.run_if_selected("slack-settings", lambda: self.migrate_slack_settings(config))
|
|
919
|
+
self.run_if_selected("whatsapp-settings", lambda: self.migrate_whatsapp_settings(config))
|
|
920
|
+
self.run_if_selected("signal-settings", lambda: self.migrate_signal_settings(config))
|
|
921
|
+
self.run_if_selected("provider-keys", lambda: self.handle_provider_keys(config))
|
|
922
|
+
self.run_if_selected("model-config", lambda: self.migrate_model_config(config))
|
|
923
|
+
self.run_if_selected("tts-config", lambda: self.migrate_tts_config(config))
|
|
924
|
+
self.run_if_selected("command-allowlist", self.migrate_command_allowlist)
|
|
925
|
+
self.run_if_selected("skills", self.migrate_skills)
|
|
926
|
+
self.run_if_selected("shared-skills", self.migrate_shared_skills)
|
|
927
|
+
self.run_if_selected("daily-memory", self.migrate_daily_memory)
|
|
928
|
+
self.run_if_selected(
|
|
929
|
+
"tts-assets",
|
|
930
|
+
lambda: self.copy_tree_non_destructive(
|
|
931
|
+
self.source_candidate("workspace/tts"),
|
|
932
|
+
self.target_root / "tts",
|
|
933
|
+
kind="tts-assets",
|
|
934
|
+
ignore_dir_names={".venv", "generated", "__pycache__"},
|
|
935
|
+
),
|
|
936
|
+
)
|
|
937
|
+
self.run_if_selected("archive", self.archive_docs)
|
|
938
|
+
|
|
939
|
+
# 前缀以获取裸文件名/子路径。
|
|
940
|
+
self.run_if_selected("mcp-servers", lambda: self.migrate_mcp_servers(config))
|
|
941
|
+
self.run_if_selected("plugins-config", lambda: self.migrate_plugins_config(config))
|
|
942
|
+
self.run_if_selected("cron-jobs", lambda: self.migrate_cron_jobs(config))
|
|
943
|
+
self.run_if_selected("hooks-config", lambda: self.migrate_hooks_config(config))
|
|
944
|
+
self.run_if_selected("agent-config", lambda: self.migrate_agent_config(config))
|
|
945
|
+
self.run_if_selected("gateway-config", lambda: self.migrate_gateway_config(config))
|
|
946
|
+
self.run_if_selected("session-config", lambda: self.migrate_session_config(config))
|
|
947
|
+
self.run_if_selected("full-providers", lambda: self.migrate_full_providers(config))
|
|
948
|
+
self.run_if_selected("deep-channels", lambda: self.migrate_deep_channels(config))
|
|
949
|
+
self.run_if_selected("browser-config", lambda: self.migrate_browser_config(config))
|
|
950
|
+
self.run_if_selected("tools-config", lambda: self.migrate_tools_config(config))
|
|
951
|
+
self.run_if_selected("approvals-config", lambda: self.migrate_approvals_config(config))
|
|
952
|
+
self.run_if_selected("memory-backend", lambda: self.migrate_memory_backend(config))
|
|
953
|
+
self.run_if_selected("skills-config", lambda: self.migrate_skills_config(config))
|
|
954
|
+
self.run_if_selected("ui-identity", lambda: self.migrate_ui_identity(config))
|
|
955
|
+
self.run_if_selected("logging-config", lambda: self.migrate_logging_config(config))
|
|
956
|
+
|
|
957
|
+
# Generate migration notes
|
|
958
|
+
self.generate_migration_notes()
|
|
959
|
+
|
|
960
|
+
return self.build_report()
|
|
961
|
+
|
|
962
|
+
def run_if_selected(self, option_id: str, func) -> None:
|
|
963
|
+
if not self.is_selected(option_id):
|
|
964
|
+
meta = MIGRATION_OPTION_METADATA[option_id]
|
|
965
|
+
self.record(option_id, None, None, "skipped", "Not selected for this run", option_label=meta["label"])
|
|
966
|
+
return
|
|
967
|
+
# ── v2 迁移模块 ──────────────────────────────
|
|
968
|
+
# 如果之前的 config.yaml 写入在应用期间遇到冲突/错误,
|
|
969
|
+
# 跳过剩余配置修改选项而非冒险部分写入。
|
|
970
|
+
# Dry-run 模式永不阻止 —— 用户需要完整预览
|
|
971
|
+
if (
|
|
972
|
+
self.execute
|
|
973
|
+
and self._config_apply_blocked
|
|
974
|
+
and option_id in self._CONFIG_MUTATING_OPTIONS
|
|
975
|
+
):
|
|
976
|
+
meta = MIGRATION_OPTION_METADATA[option_id]
|
|
977
|
+
self.record(
|
|
978
|
+
option_id,
|
|
979
|
+
None,
|
|
980
|
+
None,
|
|
981
|
+
STATUS_SKIPPED,
|
|
982
|
+
REASON_BLOCKED_BY_APPLY_CONFLICT,
|
|
983
|
+
option_label=meta["label"],
|
|
984
|
+
)
|
|
985
|
+
return
|
|
986
|
+
func()
|
|
987
|
+
|
|
988
|
+
def build_report(self) -> Dict[str, Any]:
|
|
989
|
+
summary: Dict[str, int] = {
|
|
990
|
+
"migrated": 0,
|
|
991
|
+
"archived": 0,
|
|
992
|
+
"skipped": 0,
|
|
993
|
+
"conflict": 0,
|
|
994
|
+
"error": 0,
|
|
995
|
+
}
|
|
996
|
+
for item in self.items:
|
|
997
|
+
summary[item.status] = summary.get(item.status, 0) + 1
|
|
998
|
+
|
|
999
|
+
report = {
|
|
1000
|
+
"timestamp": self.timestamp,
|
|
1001
|
+
"mode": "execute" if self.execute else "dry-run",
|
|
1002
|
+
"source_root": str(self.source_root),
|
|
1003
|
+
"target_root": str(self.target_root),
|
|
1004
|
+
"workspace_target": str(self.workspace_target) if self.workspace_target else None,
|
|
1005
|
+
"output_dir": str(self.output_dir) if self.output_dir else None,
|
|
1006
|
+
"migrate_secrets": self.migrate_secrets,
|
|
1007
|
+
"preset": self.preset_name or None,
|
|
1008
|
+
"skill_conflict_mode": self.skill_conflict_mode,
|
|
1009
|
+
"selection": {
|
|
1010
|
+
"selected": sorted(self.selected_options),
|
|
1011
|
+
"preset": self.preset_name or None,
|
|
1012
|
+
"skill_conflict_mode": self.skill_conflict_mode,
|
|
1013
|
+
"available": [
|
|
1014
|
+
{"id": option_id, **meta}
|
|
1015
|
+
for option_id, meta in MIGRATION_OPTION_METADATA.items()
|
|
1016
|
+
],
|
|
1017
|
+
"presets": [
|
|
1018
|
+
{"id": preset_id, "selected": sorted(option_ids)}
|
|
1019
|
+
for preset_id, option_ids in MIGRATION_PRESETS.items()
|
|
1020
|
+
],
|
|
1021
|
+
},
|
|
1022
|
+
"summary": summary,
|
|
1023
|
+
"items": [asdict(item) for item in self.items],
|
|
1024
|
+
"warnings": self._build_warnings(summary),
|
|
1025
|
+
"next_steps": self._build_next_steps(summary),
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if self.output_dir:
|
|
1029
|
+
write_report(self.output_dir, report)
|
|
1030
|
+
|
|
1031
|
+
return report
|
|
1032
|
+
|
|
1033
|
+
def _build_warnings(self, summary: Dict[str, int]) -> List[str]:
|
|
1034
|
+
"""Structured warnings surfaced on the report for downstream consumers.
|
|
1035
|
+
|
|
1036
|
+
Modelled on OpenClaw's extensions/migrate-hermes/plan.ts warnings[].
|
|
1037
|
+
Keep the messages actionable — they show up in summary.md and the
|
|
1038
|
+
JSON report.
|
|
1039
|
+
"""
|
|
1040
|
+
warnings: List[str] = []
|
|
1041
|
+
if summary.get("conflict", 0) > 0:
|
|
1042
|
+
warnings.append(
|
|
1043
|
+
"Conflicts were found. Re-run with --overwrite to replace conflicting "
|
|
1044
|
+
"targets after item-level backups."
|
|
1045
|
+
)
|
|
1046
|
+
if summary.get("error", 0) > 0:
|
|
1047
|
+
warnings.append(
|
|
1048
|
+
"One or more items failed. Inspect the report and re-run after fixing "
|
|
1049
|
+
"the underlying cause."
|
|
1050
|
+
)
|
|
1051
|
+
if self._config_apply_blocked and self.execute:
|
|
1052
|
+
warnings.append(
|
|
1053
|
+
"A config.yaml write hit a conflict or error mid-apply; later config "
|
|
1054
|
+
"items were skipped to avoid a partial write."
|
|
1055
|
+
)
|
|
1056
|
+
# 来决定如何继续。
|
|
1057
|
+
provider_keys_skipped = any(
|
|
1058
|
+
item.kind == "provider-keys" and item.status == STATUS_SKIPPED
|
|
1059
|
+
for item in self.items
|
|
1060
|
+
)
|
|
1061
|
+
if provider_keys_skipped and not self.migrate_secrets:
|
|
1062
|
+
warnings.append(
|
|
1063
|
+
"API keys and other credentials were detected but not imported. "
|
|
1064
|
+
"Re-run with --migrate-secrets to copy supported keys into the "
|
|
1065
|
+
"Hermes env file."
|
|
1066
|
+
)
|
|
1067
|
+
return warnings
|
|
1068
|
+
|
|
1069
|
+
def _build_next_steps(self, summary: Dict[str, int]) -> List[str]:
|
|
1070
|
+
"""Human-readable next-step guidance baked into the report."""
|
|
1071
|
+
if not self.execute:
|
|
1072
|
+
return [
|
|
1073
|
+
"Re-run without --dry-run to apply the migration.",
|
|
1074
|
+
"Pass --overwrite to resolve conflicts, or --migrate-secrets to "
|
|
1075
|
+
"include API keys.",
|
|
1076
|
+
]
|
|
1077
|
+
steps: List[str] = []
|
|
1078
|
+
if summary.get("migrated", 0) > 0:
|
|
1079
|
+
steps.append(
|
|
1080
|
+
"Review the migration report at "
|
|
1081
|
+
f"{self.output_dir}/summary.md"
|
|
1082
|
+
if self.output_dir
|
|
1083
|
+
else "Review the migration report."
|
|
1084
|
+
)
|
|
1085
|
+
steps.append(
|
|
1086
|
+
"Start a new Hermes session (or /reset) to pick up the imported config."
|
|
1087
|
+
)
|
|
1088
|
+
if summary.get("conflict", 0) > 0:
|
|
1089
|
+
steps.append(
|
|
1090
|
+
"Re-run with --overwrite to apply items that were blocked by conflicts."
|
|
1091
|
+
)
|
|
1092
|
+
return steps
|
|
1093
|
+
|
|
1094
|
+
def maybe_backup(self, path: Path) -> Optional[Path]:
|
|
1095
|
+
if not self.execute or not self.backup_dir or not path.exists():
|
|
1096
|
+
return None
|
|
1097
|
+
return backup_existing(path, self.backup_dir)
|
|
1098
|
+
|
|
1099
|
+
def write_overflow_entries(self, kind: str, entries: Sequence[str]) -> Optional[Path]:
|
|
1100
|
+
if not entries or not self.overflow_dir:
|
|
1101
|
+
return None
|
|
1102
|
+
self.overflow_dir.mkdir(parents=True, exist_ok=True)
|
|
1103
|
+
filename = f"{kind.replace('-', '_')}_overflow.txt"
|
|
1104
|
+
path = self.overflow_dir / filename
|
|
1105
|
+
path.write_text("\n".join(entries) + "\n", encoding="utf-8")
|
|
1106
|
+
return path
|
|
1107
|
+
|
|
1108
|
+
def copy_file(self, source: Path, destination: Path, kind: str,
|
|
1109
|
+
transform: Optional[Any] = None) -> None:
|
|
1110
|
+
if not source or not source.exists():
|
|
1111
|
+
return
|
|
1112
|
+
|
|
1113
|
+
if destination.exists():
|
|
1114
|
+
if not transform and sha256_file(source) == sha256_file(destination):
|
|
1115
|
+
self.record(kind, source, destination, "skipped", "Target already matches source")
|
|
1116
|
+
return
|
|
1117
|
+
if not self.overwrite:
|
|
1118
|
+
self.record(kind, source, destination, "conflict", "Target exists and overwrite is disabled")
|
|
1119
|
+
return
|
|
1120
|
+
|
|
1121
|
+
if self.execute:
|
|
1122
|
+
backup_path = self.maybe_backup(destination)
|
|
1123
|
+
ensure_parent(destination)
|
|
1124
|
+
if transform:
|
|
1125
|
+
content = read_text(source)
|
|
1126
|
+
content = transform(content)
|
|
1127
|
+
destination.write_text(content, encoding="utf-8")
|
|
1128
|
+
shutil.copystat(source, destination)
|
|
1129
|
+
else:
|
|
1130
|
+
shutil.copy2(source, destination)
|
|
1131
|
+
self.record(kind, source, destination, "migrated", backup=str(backup_path) if backup_path else None)
|
|
1132
|
+
else:
|
|
1133
|
+
self.record(kind, source, destination, "migrated", "Would copy")
|
|
1134
|
+
|
|
1135
|
+
def migrate_soul(self) -> None:
|
|
1136
|
+
source = self.source_candidate("workspace/SOUL.md", "workspace.default/SOUL.md")
|
|
1137
|
+
if not source:
|
|
1138
|
+
self.record("soul", None, self.target_root / "SOUL.md", "skipped", "No OpenClaw SOUL.md found")
|
|
1139
|
+
return
|
|
1140
|
+
self.copy_file(source, self.target_root / "SOUL.md", kind="soul", transform=rebrand_text)
|
|
1141
|
+
|
|
1142
|
+
def migrate_workspace_agents(self) -> None:
|
|
1143
|
+
source = self.source_candidate(
|
|
1144
|
+
f"workspace/{WORKSPACE_INSTRUCTIONS_FILENAME}",
|
|
1145
|
+
f"workspace.default/{WORKSPACE_INSTRUCTIONS_FILENAME}",
|
|
1146
|
+
)
|
|
1147
|
+
if source is None:
|
|
1148
|
+
self.record("workspace-agents", "workspace/AGENTS.md", "", "skipped", "Source file not found")
|
|
1149
|
+
return
|
|
1150
|
+
if not self.workspace_target:
|
|
1151
|
+
self.record("workspace-agents", source, None, "skipped", "No workspace target was provided")
|
|
1152
|
+
return
|
|
1153
|
+
destination = self.workspace_target / WORKSPACE_INSTRUCTIONS_FILENAME
|
|
1154
|
+
self.copy_file(source, destination, kind="workspace-agents", transform=rebrand_text)
|
|
1155
|
+
|
|
1156
|
+
def migrate_memory(self, source: Optional[Path], destination: Path, limit: int, kind: str) -> None:
|
|
1157
|
+
if not source or not source.exists():
|
|
1158
|
+
self.record(kind, None, destination, "skipped", "Source file not found")
|
|
1159
|
+
return
|
|
1160
|
+
|
|
1161
|
+
incoming = extract_markdown_entries(read_text(source))
|
|
1162
|
+
if not incoming:
|
|
1163
|
+
self.record(kind, source, destination, "skipped", "No importable entries found")
|
|
1164
|
+
return
|
|
1165
|
+
incoming = [rebrand_text(entry) for entry in incoming]
|
|
1166
|
+
|
|
1167
|
+
existing = parse_existing_memory_entries(destination)
|
|
1168
|
+
merged, stats, overflowed = merge_entries(existing, incoming, limit)
|
|
1169
|
+
details = {
|
|
1170
|
+
"existing_entries": stats["existing"],
|
|
1171
|
+
"added_entries": stats["added"],
|
|
1172
|
+
"duplicate_entries": stats["duplicates"],
|
|
1173
|
+
"overflowed_entries": stats["overflowed"],
|
|
1174
|
+
"char_limit": limit,
|
|
1175
|
+
"final_char_count": len(ENTRY_DELIMITER.join(merged)) if merged else 0,
|
|
1176
|
+
}
|
|
1177
|
+
overflow_file = self.write_overflow_entries(kind, overflowed)
|
|
1178
|
+
if overflow_file is not None:
|
|
1179
|
+
details["overflow_file"] = str(overflow_file)
|
|
1180
|
+
|
|
1181
|
+
if self.execute:
|
|
1182
|
+
if stats["added"] == 0 and not overflowed:
|
|
1183
|
+
self.record(kind, source, destination, "skipped", "No new entries to import", **details)
|
|
1184
|
+
return
|
|
1185
|
+
backup_path = self.maybe_backup(destination)
|
|
1186
|
+
ensure_parent(destination)
|
|
1187
|
+
destination.write_text(ENTRY_DELIMITER.join(merged) + ("\n" if merged else ""), encoding="utf-8")
|
|
1188
|
+
self.record(
|
|
1189
|
+
kind,
|
|
1190
|
+
source,
|
|
1191
|
+
destination,
|
|
1192
|
+
"migrated",
|
|
1193
|
+
backup=str(backup_path) if backup_path else "",
|
|
1194
|
+
overflow_preview=overflowed[:5],
|
|
1195
|
+
**details,
|
|
1196
|
+
)
|
|
1197
|
+
else:
|
|
1198
|
+
self.record(kind, source, destination, "migrated", "Would merge entries", overflow_preview=overflowed[:5], **details)
|
|
1199
|
+
|
|
1200
|
+
def migrate_command_allowlist(self) -> None:
|
|
1201
|
+
source = self.source_root / "exec-approvals.json"
|
|
1202
|
+
destination = self.target_root / "config.yaml"
|
|
1203
|
+
if not source.exists():
|
|
1204
|
+
self.record("command-allowlist", None, destination, "skipped", "No OpenClaw exec approvals file found")
|
|
1205
|
+
return
|
|
1206
|
+
if yaml is None:
|
|
1207
|
+
self.record("command-allowlist", source, destination, "error", "PyYAML is not available")
|
|
1208
|
+
return
|
|
1209
|
+
|
|
1210
|
+
try:
|
|
1211
|
+
data = json.loads(source.read_text(encoding="utf-8"))
|
|
1212
|
+
except json.JSONDecodeError as exc:
|
|
1213
|
+
self.record("command-allowlist", source, destination, "error", f"Invalid JSON: {exc}")
|
|
1214
|
+
return
|
|
1215
|
+
|
|
1216
|
+
patterns: List[str] = []
|
|
1217
|
+
agents = data.get("agents", {})
|
|
1218
|
+
if isinstance(agents, dict):
|
|
1219
|
+
for agent_data in agents.values():
|
|
1220
|
+
allowlist = agent_data.get("allowlist", []) if isinstance(agent_data, dict) else []
|
|
1221
|
+
for entry in allowlist:
|
|
1222
|
+
pattern = entry.get("pattern") if isinstance(entry, dict) else None
|
|
1223
|
+
if pattern:
|
|
1224
|
+
patterns.append(pattern)
|
|
1225
|
+
|
|
1226
|
+
patterns = sorted(dict.fromkeys(patterns))
|
|
1227
|
+
if not patterns:
|
|
1228
|
+
self.record("command-allowlist", source, destination, "skipped", "No allowlist patterns found")
|
|
1229
|
+
return
|
|
1230
|
+
if not destination.exists():
|
|
1231
|
+
self.record("command-allowlist", source, destination, "skipped", "Hermes config.yaml does not exist yet")
|
|
1232
|
+
return
|
|
1233
|
+
|
|
1234
|
+
config = load_yaml_file(destination)
|
|
1235
|
+
current = config.get("command_allowlist", [])
|
|
1236
|
+
if not isinstance(current, list):
|
|
1237
|
+
current = []
|
|
1238
|
+
merged = sorted(dict.fromkeys(list(current) + patterns))
|
|
1239
|
+
added = [pattern for pattern in merged if pattern not in current]
|
|
1240
|
+
if not added:
|
|
1241
|
+
self.record("command-allowlist", source, destination, "skipped", "All patterns already present")
|
|
1242
|
+
return
|
|
1243
|
+
|
|
1244
|
+
if self.execute:
|
|
1245
|
+
backup_path = self.maybe_backup(destination)
|
|
1246
|
+
config["command_allowlist"] = merged
|
|
1247
|
+
dump_yaml_file(destination, config)
|
|
1248
|
+
self.record(
|
|
1249
|
+
"command-allowlist",
|
|
1250
|
+
source,
|
|
1251
|
+
destination,
|
|
1252
|
+
"migrated",
|
|
1253
|
+
backup=str(backup_path) if backup_path else "",
|
|
1254
|
+
added_patterns=added,
|
|
1255
|
+
)
|
|
1256
|
+
else:
|
|
1257
|
+
self.record("command-allowlist", source, destination, "migrated", "Would merge patterns", added_patterns=added)
|
|
1258
|
+
|
|
1259
|
+
def load_openclaw_config(self) -> Dict[str, Any]:
|
|
1260
|
+
# 检测是否检测到了密钥但未迁移。
|
|
1261
|
+
for name in ("openclaw.json", "clawdbot.json", "moltbot.json"):
|
|
1262
|
+
config_path = self.source_root / name
|
|
1263
|
+
if config_path.exists():
|
|
1264
|
+
try:
|
|
1265
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1266
|
+
return data if isinstance(data, dict) else {}
|
|
1267
|
+
except json.JSONDecodeError:
|
|
1268
|
+
continue
|
|
1269
|
+
return {}
|
|
1270
|
+
|
|
1271
|
+
def load_openclaw_env(self) -> Dict[str, str]:
|
|
1272
|
+
"""Load the OpenClaw .env file for secrets that live there instead of config."""
|
|
1273
|
+
return parse_env_file(self.source_root / ".env")
|
|
1274
|
+
|
|
1275
|
+
def merge_env_values(self, additions: Dict[str, str], kind: str, source: Path) -> None:
|
|
1276
|
+
destination = self.target_root / ".env"
|
|
1277
|
+
env_data = parse_env_file(destination)
|
|
1278
|
+
added: Dict[str, str] = {}
|
|
1279
|
+
conflicts: List[str] = []
|
|
1280
|
+
|
|
1281
|
+
for key, value in additions.items():
|
|
1282
|
+
current = env_data.get(key)
|
|
1283
|
+
if current == value:
|
|
1284
|
+
continue
|
|
1285
|
+
if current and not self.overwrite:
|
|
1286
|
+
conflicts.append(key)
|
|
1287
|
+
continue
|
|
1288
|
+
env_data[key] = value
|
|
1289
|
+
added[key] = value
|
|
1290
|
+
|
|
1291
|
+
if conflicts and not added:
|
|
1292
|
+
self.record(kind, source, destination, "conflict", "Destination .env already has different values", conflicting_keys=conflicts)
|
|
1293
|
+
return
|
|
1294
|
+
if not conflicts and not added:
|
|
1295
|
+
self.record(kind, source, destination, "skipped", "All env values already present")
|
|
1296
|
+
return
|
|
1297
|
+
|
|
1298
|
+
if self.execute:
|
|
1299
|
+
backup_path = self.maybe_backup(destination)
|
|
1300
|
+
save_env_file(destination, env_data)
|
|
1301
|
+
self.record(
|
|
1302
|
+
kind,
|
|
1303
|
+
source,
|
|
1304
|
+
destination,
|
|
1305
|
+
"migrated",
|
|
1306
|
+
backup=str(backup_path) if backup_path else "",
|
|
1307
|
+
added_keys=sorted(added.keys()),
|
|
1308
|
+
conflicting_keys=conflicts,
|
|
1309
|
+
)
|
|
1310
|
+
else:
|
|
1311
|
+
self.record(
|
|
1312
|
+
kind,
|
|
1313
|
+
source,
|
|
1314
|
+
destination,
|
|
1315
|
+
"migrated",
|
|
1316
|
+
"Would merge env values",
|
|
1317
|
+
added_keys=sorted(added.keys()),
|
|
1318
|
+
conflicting_keys=conflicts,
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
def migrate_messaging_settings(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
1322
|
+
config = config or self.load_openclaw_config()
|
|
1323
|
+
additions: Dict[str, str] = {}
|
|
1324
|
+
|
|
1325
|
+
workspace = (
|
|
1326
|
+
config.get("agents", {})
|
|
1327
|
+
.get("defaults", {})
|
|
1328
|
+
.get("workspace")
|
|
1329
|
+
)
|
|
1330
|
+
if isinstance(workspace, str) and workspace.strip():
|
|
1331
|
+
ws_path = workspace.strip()
|
|
1332
|
+
# 如果 workspace 指向 OpenClaw 源目录内则跳过 ——
|
|
1333
|
+
# 该路径在迁移后会过期,
|
|
1334
|
+
# 会导致 Hermes 网关使用旧的 OpenClaw workspace
|
|
1335
|
+
# 作为其 cwd,拾取 OpenClaw 的
|
|
1336
|
+
try:
|
|
1337
|
+
inside_source = Path(ws_path).resolve().is_relative_to(self.source_root.resolve())
|
|
1338
|
+
except (ValueError, OSError):
|
|
1339
|
+
inside_source = False
|
|
1340
|
+
if not inside_source:
|
|
1341
|
+
additions["MESSAGING_CWD"] = ws_path
|
|
1342
|
+
|
|
1343
|
+
allowlist_path = self.source_root / "credentials" / "telegram-default-allowFrom.json"
|
|
1344
|
+
if allowlist_path.exists():
|
|
1345
|
+
try:
|
|
1346
|
+
allow_data = json.loads(allowlist_path.read_text(encoding="utf-8"))
|
|
1347
|
+
except json.JSONDecodeError:
|
|
1348
|
+
self.record("messaging-settings", allowlist_path, self.target_root / ".env", "error", "Invalid JSON in Telegram allowlist file")
|
|
1349
|
+
else:
|
|
1350
|
+
allow_from = allow_data.get("allowFrom", [])
|
|
1351
|
+
if isinstance(allow_from, list):
|
|
1352
|
+
users = [str(user).strip() for user in allow_from if str(user).strip()]
|
|
1353
|
+
if users:
|
|
1354
|
+
additions["TELEGRAM_ALLOWED_USERS"] = ",".join(users)
|
|
1355
|
+
|
|
1356
|
+
if additions:
|
|
1357
|
+
self.merge_env_values(additions, "messaging-settings", self.source_root / "openclaw.json")
|
|
1358
|
+
else:
|
|
1359
|
+
self.record("messaging-settings", self.source_root / "openclaw.json", self.target_root / ".env", "skipped", "No Hermes-compatible messaging settings found")
|
|
1360
|
+
|
|
1361
|
+
def handle_secret_settings(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
1362
|
+
config = config or self.load_openclaw_config()
|
|
1363
|
+
if self.migrate_secrets:
|
|
1364
|
+
self.migrate_secret_settings(config)
|
|
1365
|
+
return
|
|
1366
|
+
|
|
1367
|
+
config_path = self.source_root / "openclaw.json"
|
|
1368
|
+
if config_path.exists():
|
|
1369
|
+
self.record(
|
|
1370
|
+
"secret-settings",
|
|
1371
|
+
config_path,
|
|
1372
|
+
self.target_root / ".env",
|
|
1373
|
+
"skipped",
|
|
1374
|
+
"Secret migration disabled. Re-run with --migrate-secrets to import allowlisted secrets.",
|
|
1375
|
+
supported_targets=sorted(SUPPORTED_SECRET_TARGETS),
|
|
1376
|
+
)
|
|
1377
|
+
else:
|
|
1378
|
+
self.record(
|
|
1379
|
+
"secret-settings",
|
|
1380
|
+
config_path,
|
|
1381
|
+
self.target_root / ".env",
|
|
1382
|
+
"skipped",
|
|
1383
|
+
"OpenClaw config file not found",
|
|
1384
|
+
supported_targets=sorted(SUPPORTED_SECRET_TARGETS),
|
|
1385
|
+
)
|
|
1386
|
+
|
|
1387
|
+
def migrate_secret_settings(self, config: Dict[str, Any]) -> None:
|
|
1388
|
+
secret_additions: Dict[str, str] = {}
|
|
1389
|
+
|
|
1390
|
+
tg_cfg = config.get("channels", {}).get("telegram", {})
|
|
1391
|
+
telegram_token = self._get_channel_field(tg_cfg, "botToken") if isinstance(tg_cfg, dict) else None
|
|
1392
|
+
if isinstance(telegram_token, str) and telegram_token.strip():
|
|
1393
|
+
secret_additions["TELEGRAM_BOT_TOKEN"] = telegram_token.strip()
|
|
1394
|
+
|
|
1395
|
+
if secret_additions:
|
|
1396
|
+
self.merge_env_values(secret_additions, "secret-settings", self.source_root / "openclaw.json")
|
|
1397
|
+
else:
|
|
1398
|
+
self.record(
|
|
1399
|
+
"secret-settings",
|
|
1400
|
+
self.source_root / "openclaw.json",
|
|
1401
|
+
self.target_root / ".env",
|
|
1402
|
+
"skipped",
|
|
1403
|
+
"No allowlisted Hermes-compatible secrets found",
|
|
1404
|
+
supported_targets=sorted(SUPPORTED_SECRET_TARGETS),
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
def _resolve_channel_secret(self, value: Any) -> Optional[str]:
|
|
1408
|
+
"""Resolve a channel config value that may be a SecretRef."""
|
|
1409
|
+
return resolve_secret_input(value, self.load_openclaw_env())
|
|
1410
|
+
|
|
1411
|
+
@staticmethod
|
|
1412
|
+
def _get_channel_field(ch_cfg: Dict[str, Any], field: str) -> Any:
|
|
1413
|
+
"""Get a field from channel config, checking both flat and accounts.default layout."""
|
|
1414
|
+
val = ch_cfg.get(field)
|
|
1415
|
+
if val is not None:
|
|
1416
|
+
return val
|
|
1417
|
+
accounts = ch_cfg.get("accounts")
|
|
1418
|
+
if isinstance(accounts, dict):
|
|
1419
|
+
default = accounts.get("default")
|
|
1420
|
+
if isinstance(default, dict):
|
|
1421
|
+
return default.get(field)
|
|
1422
|
+
return None
|
|
1423
|
+
|
|
1424
|
+
def migrate_discord_settings(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
1425
|
+
config = config or self.load_openclaw_config()
|
|
1426
|
+
additions: Dict[str, str] = {}
|
|
1427
|
+
discord = config.get("channels", {}).get("discord", {})
|
|
1428
|
+
if isinstance(discord, dict):
|
|
1429
|
+
token = self._get_channel_field(discord, "token")
|
|
1430
|
+
if isinstance(token, str) and token.strip():
|
|
1431
|
+
additions["DISCORD_BOT_TOKEN"] = token.strip()
|
|
1432
|
+
allow_from = self._get_channel_field(discord, "allowFrom") or []
|
|
1433
|
+
if isinstance(allow_from, list):
|
|
1434
|
+
users = [str(u).strip() for u in allow_from if str(u).strip()]
|
|
1435
|
+
if users:
|
|
1436
|
+
additions["DISCORD_ALLOWED_USERS"] = ",".join(users)
|
|
1437
|
+
if additions:
|
|
1438
|
+
self.merge_env_values(additions, "discord-settings", self.source_root / "openclaw.json")
|
|
1439
|
+
else:
|
|
1440
|
+
self.record("discord-settings", self.source_root / "openclaw.json", self.target_root / ".env", "skipped", "No Discord settings found")
|
|
1441
|
+
|
|
1442
|
+
def migrate_slack_settings(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
1443
|
+
config = config or self.load_openclaw_config()
|
|
1444
|
+
additions: Dict[str, str] = {}
|
|
1445
|
+
slack = config.get("channels", {}).get("slack", {})
|
|
1446
|
+
if isinstance(slack, dict):
|
|
1447
|
+
bot_token = self._get_channel_field(slack, "botToken")
|
|
1448
|
+
if isinstance(bot_token, str) and bot_token.strip():
|
|
1449
|
+
additions["SLACK_BOT_TOKEN"] = bot_token.strip()
|
|
1450
|
+
app_token = self._get_channel_field(slack, "appToken")
|
|
1451
|
+
if isinstance(app_token, str) and app_token.strip():
|
|
1452
|
+
additions["SLACK_APP_TOKEN"] = app_token.strip()
|
|
1453
|
+
allow_from = self._get_channel_field(slack, "allowFrom") or []
|
|
1454
|
+
if isinstance(allow_from, list):
|
|
1455
|
+
users = [str(u).strip() for u in allow_from if str(u).strip()]
|
|
1456
|
+
if users:
|
|
1457
|
+
additions["SLACK_ALLOWED_USERS"] = ",".join(users)
|
|
1458
|
+
if additions:
|
|
1459
|
+
self.merge_env_values(additions, "slack-settings", self.source_root / "openclaw.json")
|
|
1460
|
+
else:
|
|
1461
|
+
self.record("slack-settings", self.source_root / "openclaw.json", self.target_root / ".env", "skipped", "No Slack settings found")
|
|
1462
|
+
|
|
1463
|
+
def migrate_whatsapp_settings(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
1464
|
+
config = config or self.load_openclaw_config()
|
|
1465
|
+
additions: Dict[str, str] = {}
|
|
1466
|
+
whatsapp = config.get("channels", {}).get("whatsapp", {})
|
|
1467
|
+
if isinstance(whatsapp, dict):
|
|
1468
|
+
allow_from = self._get_channel_field(whatsapp, "allowFrom") or []
|
|
1469
|
+
if isinstance(allow_from, list):
|
|
1470
|
+
users = [str(u).strip() for u in allow_from if str(u).strip()]
|
|
1471
|
+
if users:
|
|
1472
|
+
additions["WHATSAPP_ALLOWED_USERS"] = ",".join(users)
|
|
1473
|
+
if additions:
|
|
1474
|
+
self.merge_env_values(additions, "whatsapp-settings", self.source_root / "openclaw.json")
|
|
1475
|
+
else:
|
|
1476
|
+
self.record("whatsapp-settings", self.source_root / "openclaw.json", self.target_root / ".env", "skipped", "No WhatsApp settings found")
|
|
1477
|
+
|
|
1478
|
+
def migrate_signal_settings(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
1479
|
+
config = config or self.load_openclaw_config()
|
|
1480
|
+
additions: Dict[str, str] = {}
|
|
1481
|
+
signal = config.get("channels", {}).get("signal", {})
|
|
1482
|
+
if isinstance(signal, dict):
|
|
1483
|
+
account = self._get_channel_field(signal, "account")
|
|
1484
|
+
if isinstance(account, str) and account.strip():
|
|
1485
|
+
additions["SIGNAL_ACCOUNT"] = account.strip()
|
|
1486
|
+
http_url = self._get_channel_field(signal, "httpUrl")
|
|
1487
|
+
if isinstance(http_url, str) and http_url.strip():
|
|
1488
|
+
additions["SIGNAL_HTTP_URL"] = http_url.strip()
|
|
1489
|
+
allow_from = self._get_channel_field(signal, "allowFrom") or []
|
|
1490
|
+
if isinstance(allow_from, list):
|
|
1491
|
+
users = [str(u).strip() for u in allow_from if str(u).strip()]
|
|
1492
|
+
if users:
|
|
1493
|
+
additions["SIGNAL_ALLOWED_USERS"] = ",".join(users)
|
|
1494
|
+
if additions:
|
|
1495
|
+
self.merge_env_values(additions, "signal-settings", self.source_root / "openclaw.json")
|
|
1496
|
+
else:
|
|
1497
|
+
self.record("signal-settings", self.source_root / "openclaw.json", self.target_root / ".env", "skipped", "No Signal settings found")
|
|
1498
|
+
|
|
1499
|
+
def handle_provider_keys(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
1500
|
+
config = config or self.load_openclaw_config()
|
|
1501
|
+
if not self.migrate_secrets:
|
|
1502
|
+
config_path = self.source_root / "openclaw.json"
|
|
1503
|
+
self.record(
|
|
1504
|
+
"provider-keys",
|
|
1505
|
+
config_path,
|
|
1506
|
+
self.target_root / ".env",
|
|
1507
|
+
"skipped",
|
|
1508
|
+
"Secret migration disabled. Re-run with --migrate-secrets to import provider API keys.",
|
|
1509
|
+
supported_targets=sorted(SUPPORTED_SECRET_TARGETS),
|
|
1510
|
+
)
|
|
1511
|
+
return
|
|
1512
|
+
self.migrate_provider_keys(config)
|
|
1513
|
+
|
|
1514
|
+
def migrate_provider_keys(self, config: Dict[str, Any]) -> None:
|
|
1515
|
+
secret_additions: Dict[str, str] = {}
|
|
1516
|
+
|
|
1517
|
+
# AGENTS.md、MEMORY.md 等。
|
|
1518
|
+
# 提取 provider API 密钥
|
|
1519
|
+
openclaw_env = self.load_openclaw_env()
|
|
1520
|
+
providers = config.get("models", {}).get("providers", {})
|
|
1521
|
+
if isinstance(providers, dict):
|
|
1522
|
+
for provider_name, provider_cfg in providers.items():
|
|
1523
|
+
if not isinstance(provider_cfg, dict):
|
|
1524
|
+
continue
|
|
1525
|
+
raw_key = provider_cfg.get("apiKey")
|
|
1526
|
+
api_key = resolve_secret_input(raw_key, openclaw_env)
|
|
1527
|
+
if not api_key:
|
|
1528
|
+
# 注意:apiKey 值可以是字符串、env 模板或 SecretRef 对象
|
|
1529
|
+
if isinstance(raw_key, dict) and raw_key.get("source") in {"file", "exec"}:
|
|
1530
|
+
self.record(
|
|
1531
|
+
"provider-keys",
|
|
1532
|
+
self.source_root / "openclaw.json",
|
|
1533
|
+
None,
|
|
1534
|
+
"skipped",
|
|
1535
|
+
f"Provider '{provider_name}' uses a {raw_key['source']}-backed SecretRef "
|
|
1536
|
+
f"that cannot be auto-migrated. Add this key manually via: hermes config set",
|
|
1537
|
+
)
|
|
1538
|
+
continue
|
|
1539
|
+
|
|
1540
|
+
base_url = provider_cfg.get("baseUrl", "")
|
|
1541
|
+
api_type = provider_cfg.get("api", "")
|
|
1542
|
+
env_var = None
|
|
1543
|
+
|
|
1544
|
+
# 如果 SecretRef 的 file/exec 源静默不可解析则警告
|
|
1545
|
+
if isinstance(base_url, str):
|
|
1546
|
+
if "openrouter" in base_url.lower():
|
|
1547
|
+
env_var = "OPENROUTER_API_KEY"
|
|
1548
|
+
elif "openai.com" in base_url.lower():
|
|
1549
|
+
env_var = "OPENAI_API_KEY"
|
|
1550
|
+
elif "anthropic" in base_url.lower():
|
|
1551
|
+
env_var = "ANTHROPIC_API_KEY"
|
|
1552
|
+
|
|
1553
|
+
# 先按 baseUrl 匹配
|
|
1554
|
+
if not env_var and isinstance(api_type, str) and api_type == "anthropic-messages":
|
|
1555
|
+
env_var = "ANTHROPIC_API_KEY"
|
|
1556
|
+
|
|
1557
|
+
# 按 api 类型匹配
|
|
1558
|
+
if not env_var:
|
|
1559
|
+
name_lower = provider_name.lower()
|
|
1560
|
+
if name_lower == "openrouter":
|
|
1561
|
+
env_var = "OPENROUTER_API_KEY"
|
|
1562
|
+
elif "openai" in name_lower:
|
|
1563
|
+
env_var = "OPENAI_API_KEY"
|
|
1564
|
+
|
|
1565
|
+
if env_var:
|
|
1566
|
+
secret_additions[env_var] = api_key
|
|
1567
|
+
|
|
1568
|
+
# 按 provider 名称匹配
|
|
1569
|
+
tts = config.get("messages", {}).get("tts", {})
|
|
1570
|
+
if isinstance(tts, dict):
|
|
1571
|
+
elevenlabs = tts.get("elevenlabs", {})
|
|
1572
|
+
if isinstance(elevenlabs, dict):
|
|
1573
|
+
el_key = elevenlabs.get("apiKey")
|
|
1574
|
+
if isinstance(el_key, str) and el_key.strip():
|
|
1575
|
+
secret_additions["ELEVENLABS_API_KEY"] = el_key.strip()
|
|
1576
|
+
openai_tts = tts.get("openai", {})
|
|
1577
|
+
if isinstance(openai_tts, dict):
|
|
1578
|
+
oai_key = openai_tts.get("apiKey")
|
|
1579
|
+
if isinstance(oai_key, str) and oai_key.strip():
|
|
1580
|
+
secret_additions["VOICE_TOOLS_OPENAI_KEY"] = oai_key.strip()
|
|
1581
|
+
|
|
1582
|
+
# 提取 TTS API 密钥
|
|
1583
|
+
# 也检查 OpenClaw .env 文件 —— 许多用户
|
|
1584
|
+
openclaw_env = self.load_openclaw_env()
|
|
1585
|
+
env_key_mapping = {
|
|
1586
|
+
"OPENROUTER_API_KEY": "OPENROUTER_API_KEY",
|
|
1587
|
+
"OPENAI_API_KEY": "OPENAI_API_KEY",
|
|
1588
|
+
"ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY",
|
|
1589
|
+
"ELEVENLABS_API_KEY": "ELEVENLABS_API_KEY",
|
|
1590
|
+
"TELEGRAM_BOT_TOKEN": "TELEGRAM_BOT_TOKEN",
|
|
1591
|
+
"DEEPSEEK_API_KEY": "DEEPSEEK_API_KEY",
|
|
1592
|
+
"GEMINI_API_KEY": "GEMINI_API_KEY",
|
|
1593
|
+
"ZAI_API_KEY": "ZAI_API_KEY",
|
|
1594
|
+
"MINIMAX_API_KEY": "MINIMAX_API_KEY",
|
|
1595
|
+
}
|
|
1596
|
+
for oc_key, hermes_key in env_key_mapping.items():
|
|
1597
|
+
val = openclaw_env.get(oc_key, "").strip()
|
|
1598
|
+
if val and hermes_key not in secret_additions:
|
|
1599
|
+
secret_additions[hermes_key] = val
|
|
1600
|
+
|
|
1601
|
+
# 某些 OpenClaw 设置将 API 密钥存在这里。
|
|
1602
|
+
# 检查 openclaw.json 的"env"子对象 ——
|
|
1603
|
+
# 键可以位于 env.<KEY> 或 env.vars.<KEY>。
|
|
1604
|
+
json_env = config.get("env")
|
|
1605
|
+
if isinstance(json_env, dict):
|
|
1606
|
+
env_vars = json_env.get("vars")
|
|
1607
|
+
sources = [json_env]
|
|
1608
|
+
if isinstance(env_vars, dict):
|
|
1609
|
+
sources.append(env_vars)
|
|
1610
|
+
for src in sources:
|
|
1611
|
+
for oc_key, hermes_key in env_key_mapping.items():
|
|
1612
|
+
val = src.get(oc_key)
|
|
1613
|
+
if isinstance(val, str) and val.strip() and hermes_key not in secret_additions:
|
|
1614
|
+
secret_additions[hermes_key] = val.strip()
|
|
1615
|
+
|
|
1616
|
+
# 检查各 agent 的 auth-profiles.json 以获取额外的凭据
|
|
1617
|
+
auth_profiles_path = self.source_root / "agents" / "main" / "agent" / "auth-profiles.json"
|
|
1618
|
+
if auth_profiles_path.exists():
|
|
1619
|
+
try:
|
|
1620
|
+
profiles = json.loads(auth_profiles_path.read_text(encoding="utf-8"))
|
|
1621
|
+
if isinstance(profiles, dict):
|
|
1622
|
+
# auth-profiles.json 将配置文件包裹在 "profiles" 键中
|
|
1623
|
+
profile_entries = profiles.get("profiles", profiles) if isinstance(profiles.get("profiles"), dict) else profiles
|
|
1624
|
+
for profile_name, profile_data in profile_entries.items():
|
|
1625
|
+
if not isinstance(profile_data, dict):
|
|
1626
|
+
continue
|
|
1627
|
+
# 规范字段是 "key","apiKey" 作为别名被接受
|
|
1628
|
+
api_key = profile_data.get("key", "") or profile_data.get("apiKey", "")
|
|
1629
|
+
if not isinstance(api_key, str) or not api_key.strip():
|
|
1630
|
+
continue
|
|
1631
|
+
name_lower = profile_name.lower()
|
|
1632
|
+
if "openrouter" in name_lower and "OPENROUTER_API_KEY" not in secret_additions:
|
|
1633
|
+
secret_additions["OPENROUTER_API_KEY"] = api_key.strip()
|
|
1634
|
+
elif "openai" in name_lower and "OPENAI_API_KEY" not in secret_additions:
|
|
1635
|
+
secret_additions["OPENAI_API_KEY"] = api_key.strip()
|
|
1636
|
+
elif "anthropic" in name_lower and "ANTHROPIC_API_KEY" not in secret_additions:
|
|
1637
|
+
secret_additions["ANTHROPIC_API_KEY"] = api_key.strip()
|
|
1638
|
+
except (json.JSONDecodeError, OSError):
|
|
1639
|
+
pass
|
|
1640
|
+
|
|
1641
|
+
if secret_additions:
|
|
1642
|
+
self.merge_env_values(secret_additions, "provider-keys", self.source_root / "openclaw.json")
|
|
1643
|
+
else:
|
|
1644
|
+
self.record(
|
|
1645
|
+
"provider-keys",
|
|
1646
|
+
self.source_root / "openclaw.json",
|
|
1647
|
+
self.target_root / ".env",
|
|
1648
|
+
"skipped",
|
|
1649
|
+
"No provider API keys found",
|
|
1650
|
+
supported_targets=sorted(SUPPORTED_SECRET_TARGETS),
|
|
1651
|
+
)
|
|
1652
|
+
|
|
1653
|
+
def migrate_model_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
1654
|
+
config = config or self.load_openclaw_config()
|
|
1655
|
+
destination = self.target_root / "config.yaml"
|
|
1656
|
+
source_path = self.source_root / "openclaw.json"
|
|
1657
|
+
|
|
1658
|
+
model_value = config.get("agents", {}).get("defaults", {}).get("model")
|
|
1659
|
+
if model_value is None:
|
|
1660
|
+
self.record("model-config", source_path, destination, "skipped", "No default model found in OpenClaw config")
|
|
1661
|
+
return
|
|
1662
|
+
|
|
1663
|
+
if isinstance(model_value, dict):
|
|
1664
|
+
model_str = model_value.get("primary")
|
|
1665
|
+
else:
|
|
1666
|
+
model_str = model_value
|
|
1667
|
+
|
|
1668
|
+
if not isinstance(model_str, str) or not model_str.strip():
|
|
1669
|
+
self.record("model-config", source_path, destination, "skipped", "Default model value is empty or invalid")
|
|
1670
|
+
return
|
|
1671
|
+
|
|
1672
|
+
model_str = model_str.strip()
|
|
1673
|
+
|
|
1674
|
+
# 根据 OpenClaw 模型目录解析模型别名。
|
|
1675
|
+
# OpenClaw 将 agents.defaults.model 存储为纯字符串或
|
|
1676
|
+
# {"primary": "<value>"},并且该值可以是:
|
|
1677
|
+
# - 完整的 provider/model API ID(例如 "anthropic/claude-opus-4-6"),或者
|
|
1678
|
+
# - 映射到 API ID 的显示别名(例如 "Claude Opus 4.6")。
|
|
1679
|
+
# agents.defaults.models 处的目录以完整的
|
|
1680
|
+
# provider/model API ID 作为键,并在值中带有 "alias" 字段,例如:
|
|
1681
|
+
# {"anthropic/claude-opus-4-6": {"alias": "Claude Opus 4.6"}}
|
|
1682
|
+
# 如果 model_str 匹配目录中的别名,将其重写为
|
|
1683
|
+
# 目录键(真正的 API ID)。如果它已经是 API ID 或者在
|
|
1684
|
+
# 目录中没有匹配项,则保持原样,让下游直接放行。
|
|
1685
|
+
model_catalog = config.get("agents", {}).get("defaults", {}).get("models", {})
|
|
1686
|
+
if isinstance(model_catalog, dict) and model_str not in model_catalog:
|
|
1687
|
+
for api_id, entry in model_catalog.items():
|
|
1688
|
+
if not isinstance(api_id, str):
|
|
1689
|
+
continue
|
|
1690
|
+
if isinstance(entry, dict) and entry.get("alias") == model_str:
|
|
1691
|
+
model_str = api_id
|
|
1692
|
+
break
|
|
1693
|
+
if isinstance(entry, str) and entry == model_str:
|
|
1694
|
+
model_str = api_id
|
|
1695
|
+
break
|
|
1696
|
+
|
|
1697
|
+
if yaml is None:
|
|
1698
|
+
self.record("model-config", source_path, destination, "error", "PyYAML is not available")
|
|
1699
|
+
return
|
|
1700
|
+
|
|
1701
|
+
hermes_config = load_yaml_file(destination)
|
|
1702
|
+
current_model = hermes_config.get("model")
|
|
1703
|
+
if current_model == model_str:
|
|
1704
|
+
self.record("model-config", source_path, destination, "skipped", "Model already set to the same value")
|
|
1705
|
+
return
|
|
1706
|
+
if current_model and not self.overwrite:
|
|
1707
|
+
self.record("model-config", source_path, destination, "conflict", "Model already set and overwrite is disabled", current=current_model, incoming=model_str)
|
|
1708
|
+
return
|
|
1709
|
+
|
|
1710
|
+
if self.execute:
|
|
1711
|
+
backup_path = self.maybe_backup(destination)
|
|
1712
|
+
existing_model = hermes_config.get("model")
|
|
1713
|
+
if isinstance(existing_model, dict):
|
|
1714
|
+
existing_model["default"] = model_str
|
|
1715
|
+
else:
|
|
1716
|
+
hermes_config["model"] = {"default": model_str}
|
|
1717
|
+
dump_yaml_file(destination, hermes_config)
|
|
1718
|
+
self.record("model-config", source_path, destination, "migrated", backup=str(backup_path) if backup_path else "", model=model_str)
|
|
1719
|
+
else:
|
|
1720
|
+
self.record("model-config", source_path, destination, "migrated", "Would set model", model=model_str)
|
|
1721
|
+
|
|
1722
|
+
def migrate_tts_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
1723
|
+
config = config or self.load_openclaw_config()
|
|
1724
|
+
destination = self.target_root / "config.yaml"
|
|
1725
|
+
source_path = self.source_root / "openclaw.json"
|
|
1726
|
+
|
|
1727
|
+
tts = config.get("messages", {}).get("tts", {})
|
|
1728
|
+
if not isinstance(tts, dict) or not tts:
|
|
1729
|
+
self.record("tts-config", source_path, destination, "skipped", "No TTS configuration found in OpenClaw config")
|
|
1730
|
+
return
|
|
1731
|
+
|
|
1732
|
+
if yaml is None:
|
|
1733
|
+
self.record("tts-config", source_path, destination, "error", "PyYAML is not available")
|
|
1734
|
+
return
|
|
1735
|
+
|
|
1736
|
+
tts_data: Dict[str, Any] = {}
|
|
1737
|
+
|
|
1738
|
+
provider = tts.get("provider")
|
|
1739
|
+
if isinstance(provider, str) and provider in {"elevenlabs", "openai", "edge", "microsoft"}:
|
|
1740
|
+
# OpenClaw 将 "edge" 重命名为 "microsoft";Hermes 仍在使用 "edge"
|
|
1741
|
+
tts_data["provider"] = "edge" if provider == "microsoft" else provider
|
|
1742
|
+
|
|
1743
|
+
# TTS 提供商设置位于 messages.tts.providers.{provider} 下
|
|
1744
|
+
# (在 OpenClaw 中,而不是直接在 messages.tts.elevenlabs 下)
|
|
1745
|
+
providers = tts.get("providers") or {}
|
|
1746
|
+
|
|
1747
|
+
# 同时检查顶层的 "talk" 配置,它也包含提供商设置
|
|
1748
|
+
talk_cfg = (config or self.load_openclaw_config()).get("talk") or {}
|
|
1749
|
+
talk_providers = talk_cfg.get("providers") or {}
|
|
1750
|
+
|
|
1751
|
+
# 合并优先级:messages.tts.providers 优先,其次是 talk.providers,
|
|
1752
|
+
# 然后是传统的扁平键(messages.tts.elevenlabs 等)
|
|
1753
|
+
elevenlabs = (
|
|
1754
|
+
(providers.get("elevenlabs") or {})
|
|
1755
|
+
if isinstance(providers.get("elevenlabs"), dict) else
|
|
1756
|
+
(talk_providers.get("elevenlabs") or {})
|
|
1757
|
+
if isinstance(talk_providers.get("elevenlabs"), dict) else
|
|
1758
|
+
(tts.get("elevenlabs") or {})
|
|
1759
|
+
)
|
|
1760
|
+
if isinstance(elevenlabs, dict):
|
|
1761
|
+
el_settings: Dict[str, str] = {}
|
|
1762
|
+
voice_id = elevenlabs.get("voiceId") or talk_cfg.get("voiceId")
|
|
1763
|
+
if isinstance(voice_id, str) and voice_id.strip():
|
|
1764
|
+
el_settings["voice_id"] = voice_id.strip()
|
|
1765
|
+
model_id = elevenlabs.get("modelId") or talk_cfg.get("modelId")
|
|
1766
|
+
if isinstance(model_id, str) and model_id.strip():
|
|
1767
|
+
el_settings["model_id"] = model_id.strip()
|
|
1768
|
+
if el_settings:
|
|
1769
|
+
tts_data["elevenlabs"] = el_settings
|
|
1770
|
+
|
|
1771
|
+
openai_tts = (
|
|
1772
|
+
(providers.get("openai") or {})
|
|
1773
|
+
if isinstance(providers.get("openai"), dict) else
|
|
1774
|
+
(talk_providers.get("openai") or {})
|
|
1775
|
+
if isinstance(talk_providers.get("openai"), dict) else
|
|
1776
|
+
(tts.get("openai") or {})
|
|
1777
|
+
)
|
|
1778
|
+
if isinstance(openai_tts, dict):
|
|
1779
|
+
oai_settings: Dict[str, str] = {}
|
|
1780
|
+
oai_model = openai_tts.get("model") or openai_tts.get("modelId")
|
|
1781
|
+
if isinstance(oai_model, str) and oai_model.strip():
|
|
1782
|
+
oai_settings["model"] = oai_model.strip()
|
|
1783
|
+
oai_voice = openai_tts.get("voice")
|
|
1784
|
+
if isinstance(oai_voice, str) and oai_voice.strip():
|
|
1785
|
+
oai_settings["voice"] = oai_voice.strip()
|
|
1786
|
+
if oai_settings:
|
|
1787
|
+
tts_data["openai"] = oai_settings
|
|
1788
|
+
|
|
1789
|
+
edge_tts = (
|
|
1790
|
+
(providers.get("edge") or providers.get("microsoft") or {})
|
|
1791
|
+
if isinstance(providers.get("edge"), dict) or isinstance(providers.get("microsoft"), dict) else
|
|
1792
|
+
(tts.get("edge") or tts.get("microsoft") or {})
|
|
1793
|
+
)
|
|
1794
|
+
if isinstance(edge_tts, dict):
|
|
1795
|
+
edge_voice = edge_tts.get("voice")
|
|
1796
|
+
if isinstance(edge_voice, str) and edge_voice.strip():
|
|
1797
|
+
tts_data["edge"] = {"voice": edge_voice.strip()}
|
|
1798
|
+
|
|
1799
|
+
if not tts_data:
|
|
1800
|
+
self.record("tts-config", source_path, destination, "skipped", "No compatible TTS settings found")
|
|
1801
|
+
return
|
|
1802
|
+
|
|
1803
|
+
hermes_config = load_yaml_file(destination)
|
|
1804
|
+
existing_tts = hermes_config.get("tts", {})
|
|
1805
|
+
if not isinstance(existing_tts, dict):
|
|
1806
|
+
existing_tts = {}
|
|
1807
|
+
|
|
1808
|
+
if self.execute:
|
|
1809
|
+
backup_path = self.maybe_backup(destination)
|
|
1810
|
+
merged_tts = dict(existing_tts)
|
|
1811
|
+
for key, value in tts_data.items():
|
|
1812
|
+
if isinstance(value, dict) and isinstance(merged_tts.get(key), dict):
|
|
1813
|
+
merged_tts[key] = {**merged_tts[key], **value}
|
|
1814
|
+
else:
|
|
1815
|
+
merged_tts[key] = value
|
|
1816
|
+
hermes_config["tts"] = merged_tts
|
|
1817
|
+
dump_yaml_file(destination, hermes_config)
|
|
1818
|
+
self.record("tts-config", source_path, destination, "migrated", backup=str(backup_path) if backup_path else "", settings=list(tts_data.keys()))
|
|
1819
|
+
else:
|
|
1820
|
+
self.record("tts-config", source_path, destination, "migrated", "Would set TTS config", settings=list(tts_data.keys()))
|
|
1821
|
+
|
|
1822
|
+
def migrate_shared_skills(self) -> None:
|
|
1823
|
+
# 检查所有 OpenClaw 技能来源:托管的、个人的、项目级别的
|
|
1824
|
+
skill_sources = [
|
|
1825
|
+
(self.source_root / "skills", "shared-skills", "managed skills"),
|
|
1826
|
+
(Path.home() / ".agents" / "skills", "personal-skills", "personal cross-project skills"),
|
|
1827
|
+
(self.source_root / "workspace" / ".agents" / "skills", "project-skills", "project-level shared skills"),
|
|
1828
|
+
(self.source_root / "workspace.default" / ".agents" / "skills", "project-skills", "project-level shared skills"),
|
|
1829
|
+
]
|
|
1830
|
+
found_any = False
|
|
1831
|
+
for source_root, kind_label, desc in skill_sources:
|
|
1832
|
+
if source_root.exists():
|
|
1833
|
+
found_any = True
|
|
1834
|
+
self._import_skill_directory(source_root, kind_label, desc)
|
|
1835
|
+
if not found_any:
|
|
1836
|
+
destination_root = self.target_root / "skills" / SKILL_CATEGORY_DIRNAME
|
|
1837
|
+
self.record("shared-skills", None, destination_root, "skipped", "No shared OpenClaw skills directories found")
|
|
1838
|
+
|
|
1839
|
+
def _import_skill_directory(self, source_root: Path, kind_label: str, desc: str) -> None:
|
|
1840
|
+
"""Import skills from a single source directory into openclaw-imports."""
|
|
1841
|
+
destination_root = self.target_root / "skills" / SKILL_CATEGORY_DIRNAME
|
|
1842
|
+
|
|
1843
|
+
skill_dirs = [p for p in sorted(source_root.iterdir()) if p.is_dir() and (p / "SKILL.md").exists()]
|
|
1844
|
+
if not skill_dirs:
|
|
1845
|
+
self.record(kind_label, source_root, destination_root, "skipped", f"No skills with SKILL.md found in {desc}")
|
|
1846
|
+
return
|
|
1847
|
+
|
|
1848
|
+
for skill_dir in skill_dirs:
|
|
1849
|
+
destination = destination_root / skill_dir.name
|
|
1850
|
+
final_destination = destination
|
|
1851
|
+
if destination.exists():
|
|
1852
|
+
if self.skill_conflict_mode == "skip":
|
|
1853
|
+
self.record(kind_label, skill_dir, destination, "conflict", "Destination skill already exists")
|
|
1854
|
+
continue
|
|
1855
|
+
if self.skill_conflict_mode == "rename":
|
|
1856
|
+
final_destination = self.resolve_skill_destination(destination)
|
|
1857
|
+
if self.execute:
|
|
1858
|
+
backup_path = None
|
|
1859
|
+
if final_destination == destination and destination.exists():
|
|
1860
|
+
backup_path = self.maybe_backup(destination)
|
|
1861
|
+
final_destination.parent.mkdir(parents=True, exist_ok=True)
|
|
1862
|
+
if final_destination == destination and destination.exists():
|
|
1863
|
+
shutil.rmtree(destination)
|
|
1864
|
+
shutil.copytree(skill_dir, final_destination)
|
|
1865
|
+
details: Dict[str, Any] = {"backup": str(backup_path) if backup_path else ""}
|
|
1866
|
+
if final_destination != destination:
|
|
1867
|
+
details["renamed_from"] = str(destination)
|
|
1868
|
+
self.record(kind_label, skill_dir, final_destination, "migrated", **details)
|
|
1869
|
+
else:
|
|
1870
|
+
if final_destination != destination:
|
|
1871
|
+
self.record(
|
|
1872
|
+
kind_label,
|
|
1873
|
+
skill_dir,
|
|
1874
|
+
final_destination,
|
|
1875
|
+
"migrated",
|
|
1876
|
+
f"Would copy {desc} directory under a renamed folder",
|
|
1877
|
+
renamed_from=str(destination),
|
|
1878
|
+
)
|
|
1879
|
+
else:
|
|
1880
|
+
self.record(kind_label, skill_dir, final_destination, "migrated", f"Would copy {desc} directory")
|
|
1881
|
+
|
|
1882
|
+
desc_path = destination_root / "DESCRIPTION.md"
|
|
1883
|
+
if self.execute:
|
|
1884
|
+
desc_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1885
|
+
if not desc_path.exists():
|
|
1886
|
+
desc_path.write_text(SKILL_CATEGORY_DESCRIPTION + "\n", encoding="utf-8")
|
|
1887
|
+
elif not desc_path.exists():
|
|
1888
|
+
self.record("shared-skill-category", None, desc_path, "migrated", "Would create category description")
|
|
1889
|
+
|
|
1890
|
+
def migrate_daily_memory(self) -> None:
|
|
1891
|
+
source_dir = self.source_candidate("workspace/memory")
|
|
1892
|
+
destination = self.target_root / "memories" / "MEMORY.md"
|
|
1893
|
+
if not source_dir or not source_dir.is_dir():
|
|
1894
|
+
self.record("daily-memory", None, destination, "skipped", "No workspace/memory/ directory found")
|
|
1895
|
+
return
|
|
1896
|
+
|
|
1897
|
+
md_files = sorted(p for p in source_dir.iterdir() if p.is_file() and p.suffix == ".md")
|
|
1898
|
+
if not md_files:
|
|
1899
|
+
self.record("daily-memory", source_dir, destination, "skipped", "No .md files found in workspace/memory/")
|
|
1900
|
+
return
|
|
1901
|
+
|
|
1902
|
+
all_incoming: List[str] = []
|
|
1903
|
+
for md_file in md_files:
|
|
1904
|
+
entries = extract_markdown_entries(read_text(md_file))
|
|
1905
|
+
all_incoming.extend(entries)
|
|
1906
|
+
|
|
1907
|
+
if not all_incoming:
|
|
1908
|
+
self.record("daily-memory", source_dir, destination, "skipped", "No importable entries found in daily memory files")
|
|
1909
|
+
return
|
|
1910
|
+
all_incoming = [rebrand_text(entry) for entry in all_incoming]
|
|
1911
|
+
|
|
1912
|
+
existing = parse_existing_memory_entries(destination)
|
|
1913
|
+
merged, stats, overflowed = merge_entries(existing, all_incoming, self.memory_limit)
|
|
1914
|
+
details = {
|
|
1915
|
+
"source_files": len(md_files),
|
|
1916
|
+
"existing_entries": stats["existing"],
|
|
1917
|
+
"added_entries": stats["added"],
|
|
1918
|
+
"duplicate_entries": stats["duplicates"],
|
|
1919
|
+
"overflowed_entries": stats["overflowed"],
|
|
1920
|
+
"char_limit": self.memory_limit,
|
|
1921
|
+
"final_char_count": len(ENTRY_DELIMITER.join(merged)) if merged else 0,
|
|
1922
|
+
}
|
|
1923
|
+
overflow_file = self.write_overflow_entries("daily-memory", overflowed)
|
|
1924
|
+
if overflow_file is not None:
|
|
1925
|
+
details["overflow_file"] = str(overflow_file)
|
|
1926
|
+
|
|
1927
|
+
if self.execute:
|
|
1928
|
+
if stats["added"] == 0 and not overflowed:
|
|
1929
|
+
self.record("daily-memory", source_dir, destination, "skipped", "No new entries to import", **details)
|
|
1930
|
+
return
|
|
1931
|
+
backup_path = self.maybe_backup(destination)
|
|
1932
|
+
ensure_parent(destination)
|
|
1933
|
+
destination.write_text(ENTRY_DELIMITER.join(merged) + ("\n" if merged else ""), encoding="utf-8")
|
|
1934
|
+
self.record(
|
|
1935
|
+
"daily-memory",
|
|
1936
|
+
source_dir,
|
|
1937
|
+
destination,
|
|
1938
|
+
"migrated",
|
|
1939
|
+
backup=str(backup_path) if backup_path else "",
|
|
1940
|
+
overflow_preview=overflowed[:5],
|
|
1941
|
+
**details,
|
|
1942
|
+
)
|
|
1943
|
+
else:
|
|
1944
|
+
self.record("daily-memory", source_dir, destination, "migrated", "Would merge daily memory entries", overflow_preview=overflowed[:5], **details)
|
|
1945
|
+
|
|
1946
|
+
def migrate_skills(self) -> None:
|
|
1947
|
+
source_root = self.source_candidate("workspace/skills")
|
|
1948
|
+
destination_root = self.target_root / "skills" / SKILL_CATEGORY_DIRNAME
|
|
1949
|
+
if not source_root or not source_root.exists():
|
|
1950
|
+
self.record("skills", None, destination_root, "skipped", "No OpenClaw skills directory found")
|
|
1951
|
+
return
|
|
1952
|
+
|
|
1953
|
+
skill_dirs = [p for p in sorted(source_root.iterdir()) if p.is_dir() and (p / "SKILL.md").exists()]
|
|
1954
|
+
if not skill_dirs:
|
|
1955
|
+
self.record("skills", source_root, destination_root, "skipped", "No skills with SKILL.md found")
|
|
1956
|
+
return
|
|
1957
|
+
|
|
1958
|
+
for skill_dir in skill_dirs:
|
|
1959
|
+
destination = destination_root / skill_dir.name
|
|
1960
|
+
final_destination = destination
|
|
1961
|
+
if destination.exists():
|
|
1962
|
+
if self.skill_conflict_mode == "skip":
|
|
1963
|
+
self.record("skill", skill_dir, destination, "conflict", "Destination skill already exists")
|
|
1964
|
+
continue
|
|
1965
|
+
if self.skill_conflict_mode == "rename":
|
|
1966
|
+
final_destination = self.resolve_skill_destination(destination)
|
|
1967
|
+
if self.execute:
|
|
1968
|
+
backup_path = None
|
|
1969
|
+
if final_destination == destination and destination.exists():
|
|
1970
|
+
backup_path = self.maybe_backup(destination)
|
|
1971
|
+
final_destination.parent.mkdir(parents=True, exist_ok=True)
|
|
1972
|
+
if final_destination == destination and destination.exists():
|
|
1973
|
+
shutil.rmtree(destination)
|
|
1974
|
+
shutil.copytree(skill_dir, final_destination)
|
|
1975
|
+
details: Dict[str, Any] = {"backup": str(backup_path) if backup_path else ""}
|
|
1976
|
+
if final_destination != destination:
|
|
1977
|
+
details["renamed_from"] = str(destination)
|
|
1978
|
+
self.record("skill", skill_dir, final_destination, "migrated", **details)
|
|
1979
|
+
else:
|
|
1980
|
+
if final_destination != destination:
|
|
1981
|
+
self.record(
|
|
1982
|
+
"skill",
|
|
1983
|
+
skill_dir,
|
|
1984
|
+
final_destination,
|
|
1985
|
+
"migrated",
|
|
1986
|
+
"Would copy skill directory under a renamed folder",
|
|
1987
|
+
renamed_from=str(destination),
|
|
1988
|
+
)
|
|
1989
|
+
else:
|
|
1990
|
+
self.record("skill", skill_dir, final_destination, "migrated", "Would copy skill directory")
|
|
1991
|
+
|
|
1992
|
+
desc_path = destination_root / "DESCRIPTION.md"
|
|
1993
|
+
if self.execute:
|
|
1994
|
+
desc_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1995
|
+
if not desc_path.exists():
|
|
1996
|
+
desc_path.write_text(SKILL_CATEGORY_DESCRIPTION + "\n", encoding="utf-8")
|
|
1997
|
+
elif not desc_path.exists():
|
|
1998
|
+
self.record("skill-category", None, desc_path, "migrated", "Would create category description")
|
|
1999
|
+
|
|
2000
|
+
def copy_tree_non_destructive(
|
|
2001
|
+
self,
|
|
2002
|
+
source_root: Optional[Path],
|
|
2003
|
+
destination_root: Path,
|
|
2004
|
+
kind: str,
|
|
2005
|
+
ignore_dir_names: Optional[set[str]] = None,
|
|
2006
|
+
) -> None:
|
|
2007
|
+
if not source_root or not source_root.exists():
|
|
2008
|
+
self.record(kind, None, destination_root, "skipped", "Source directory not found")
|
|
2009
|
+
return
|
|
2010
|
+
|
|
2011
|
+
ignore_dir_names = ignore_dir_names or set()
|
|
2012
|
+
files = [
|
|
2013
|
+
p
|
|
2014
|
+
for p in source_root.rglob("*")
|
|
2015
|
+
if p.is_file() and not any(part in ignore_dir_names for part in p.relative_to(source_root).parts[:-1])
|
|
2016
|
+
]
|
|
2017
|
+
if not files:
|
|
2018
|
+
self.record(kind, source_root, destination_root, "skipped", "No files found")
|
|
2019
|
+
return
|
|
2020
|
+
|
|
2021
|
+
copied = 0
|
|
2022
|
+
skipped = 0
|
|
2023
|
+
conflicts = 0
|
|
2024
|
+
|
|
2025
|
+
for source in files:
|
|
2026
|
+
rel = source.relative_to(source_root)
|
|
2027
|
+
destination = destination_root / rel
|
|
2028
|
+
if destination.exists():
|
|
2029
|
+
if sha256_file(source) == sha256_file(destination):
|
|
2030
|
+
skipped += 1
|
|
2031
|
+
continue
|
|
2032
|
+
if not self.overwrite:
|
|
2033
|
+
conflicts += 1
|
|
2034
|
+
self.record(kind, source, destination, "conflict", "Destination file already exists")
|
|
2035
|
+
continue
|
|
2036
|
+
|
|
2037
|
+
if self.execute:
|
|
2038
|
+
self.maybe_backup(destination)
|
|
2039
|
+
ensure_parent(destination)
|
|
2040
|
+
shutil.copy2(source, destination)
|
|
2041
|
+
copied += 1
|
|
2042
|
+
|
|
2043
|
+
status = "migrated" if copied else "skipped"
|
|
2044
|
+
reason = ""
|
|
2045
|
+
if not copied and conflicts:
|
|
2046
|
+
status = "conflict"
|
|
2047
|
+
reason = "All candidate files conflicted with existing destination files"
|
|
2048
|
+
elif not copied:
|
|
2049
|
+
reason = "No new files to copy"
|
|
2050
|
+
|
|
2051
|
+
self.record(kind, source_root, destination_root, status, reason, copied_files=copied, unchanged_files=skipped, conflicts=conflicts)
|
|
2052
|
+
|
|
2053
|
+
def archive_docs(self) -> None:
|
|
2054
|
+
candidates = [
|
|
2055
|
+
self.source_candidate("workspace/IDENTITY.md", "workspace.default/IDENTITY.md"),
|
|
2056
|
+
self.source_candidate("workspace/TOOLS.md", "workspace.default/TOOLS.md"),
|
|
2057
|
+
self.source_candidate("workspace/HEARTBEAT.md", "workspace.default/HEARTBEAT.md"),
|
|
2058
|
+
self.source_candidate("workspace/BOOTSTRAP.md", "workspace.default/BOOTSTRAP.md"),
|
|
2059
|
+
]
|
|
2060
|
+
for candidate in candidates:
|
|
2061
|
+
if candidate:
|
|
2062
|
+
self.archive_path(candidate, reason="No direct Hermes destination; archived for manual review")
|
|
2063
|
+
|
|
2064
|
+
for rel in ("workspace/.learnings", "workspace/memory"):
|
|
2065
|
+
candidate = self.source_root / rel
|
|
2066
|
+
if candidate.exists():
|
|
2067
|
+
self.archive_path(candidate, reason="No direct Hermes destination; archived for manual review")
|
|
2068
|
+
|
|
2069
|
+
partially_extracted = [
|
|
2070
|
+
("openclaw.json", "Selected Hermes-compatible values were extracted; raw OpenClaw config was not copied."),
|
|
2071
|
+
("credentials/telegram-default-allowFrom.json", "Selected Hermes-compatible values were extracted; raw credentials file was not copied."),
|
|
2072
|
+
]
|
|
2073
|
+
for rel, reason in partially_extracted:
|
|
2074
|
+
candidate = self.source_root / rel
|
|
2075
|
+
if candidate.exists():
|
|
2076
|
+
self.record("raw-config-skip", candidate, None, "skipped", reason)
|
|
2077
|
+
|
|
2078
|
+
skipped_sensitive = [
|
|
2079
|
+
"memory/main.sqlite",
|
|
2080
|
+
"credentials",
|
|
2081
|
+
"devices",
|
|
2082
|
+
"identity",
|
|
2083
|
+
"workspace.zip",
|
|
2084
|
+
]
|
|
2085
|
+
for rel in skipped_sensitive:
|
|
2086
|
+
candidate = self.source_root / rel
|
|
2087
|
+
if candidate.exists():
|
|
2088
|
+
self.record("sensitive-skip", candidate, None, "skipped", "Contains secrets, binary state, or product-specific runtime data")
|
|
2089
|
+
|
|
2090
|
+
def archive_path(self, source: Path, reason: str) -> None:
|
|
2091
|
+
destination = self.archive_dir / relative_label(source, self.source_root) if self.archive_dir else None
|
|
2092
|
+
if self.execute and destination is not None:
|
|
2093
|
+
ensure_parent(destination)
|
|
2094
|
+
if source.is_dir():
|
|
2095
|
+
shutil.copytree(source, destination, dirs_exist_ok=True)
|
|
2096
|
+
else:
|
|
2097
|
+
shutil.copy2(source, destination)
|
|
2098
|
+
self.record("archive", source, destination, "archived", reason)
|
|
2099
|
+
else:
|
|
2100
|
+
self.record("archive", source, destination, "archived", reason)
|
|
2101
|
+
|
|
2102
|
+
# ── MCP 服务器 ─────────────────────────────────────────────
|
|
2103
|
+
def migrate_mcp_servers(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2104
|
+
config = config or self.load_openclaw_config()
|
|
2105
|
+
mcp_raw = (config.get("mcp") or {}).get("servers") or {}
|
|
2106
|
+
if not mcp_raw:
|
|
2107
|
+
self.record("mcp-servers", None, None, "skipped", "No MCP servers found in OpenClaw config")
|
|
2108
|
+
return
|
|
2109
|
+
|
|
2110
|
+
hermes_cfg_path = self.target_root / "config.yaml"
|
|
2111
|
+
hermes_cfg = load_yaml_file(hermes_cfg_path)
|
|
2112
|
+
existing_mcp = hermes_cfg.get("mcp_servers") or {}
|
|
2113
|
+
added = 0
|
|
2114
|
+
|
|
2115
|
+
for name, srv in mcp_raw.items():
|
|
2116
|
+
if not isinstance(srv, dict):
|
|
2117
|
+
continue
|
|
2118
|
+
if name in existing_mcp and not self.overwrite:
|
|
2119
|
+
self.record("mcp-servers", f"mcp.servers.{name}", f"mcp_servers.{name}", "conflict",
|
|
2120
|
+
"MCP server already exists in Hermes config")
|
|
2121
|
+
continue
|
|
2122
|
+
|
|
2123
|
+
hermes_srv: Dict[str, Any] = {}
|
|
2124
|
+
# STDIO 传输
|
|
2125
|
+
if srv.get("command"):
|
|
2126
|
+
hermes_srv["command"] = srv["command"]
|
|
2127
|
+
if srv.get("args"):
|
|
2128
|
+
hermes_srv["args"] = srv["args"]
|
|
2129
|
+
if srv.get("env"):
|
|
2130
|
+
hermes_srv["env"] = srv["env"]
|
|
2131
|
+
if srv.get("cwd"):
|
|
2132
|
+
hermes_srv["cwd"] = srv["cwd"]
|
|
2133
|
+
# HTTP/SSE 传输
|
|
2134
|
+
if srv.get("url"):
|
|
2135
|
+
hermes_srv["url"] = srv["url"]
|
|
2136
|
+
if srv.get("headers"):
|
|
2137
|
+
hermes_srv["headers"] = srv["headers"]
|
|
2138
|
+
if srv.get("auth"):
|
|
2139
|
+
hermes_srv["auth"] = srv["auth"]
|
|
2140
|
+
# 公共字段
|
|
2141
|
+
if srv.get("enabled") is False:
|
|
2142
|
+
hermes_srv["enabled"] = False
|
|
2143
|
+
if srv.get("timeout"):
|
|
2144
|
+
hermes_srv["timeout"] = srv["timeout"]
|
|
2145
|
+
if srv.get("connectTimeout"):
|
|
2146
|
+
hermes_srv["connect_timeout"] = srv["connectTimeout"]
|
|
2147
|
+
# 工具过滤
|
|
2148
|
+
tools_cfg = srv.get("tools") or {}
|
|
2149
|
+
if tools_cfg.get("include") or tools_cfg.get("exclude"):
|
|
2150
|
+
hermes_srv["tools"] = {}
|
|
2151
|
+
if tools_cfg.get("include"):
|
|
2152
|
+
hermes_srv["tools"]["include"] = tools_cfg["include"]
|
|
2153
|
+
if tools_cfg.get("exclude"):
|
|
2154
|
+
hermes_srv["tools"]["exclude"] = tools_cfg["exclude"]
|
|
2155
|
+
# 采样
|
|
2156
|
+
sampling = srv.get("sampling")
|
|
2157
|
+
if sampling and isinstance(sampling, dict):
|
|
2158
|
+
hermes_srv["sampling"] = {
|
|
2159
|
+
k: v for k, v in {
|
|
2160
|
+
"enabled": sampling.get("enabled"),
|
|
2161
|
+
"model": sampling.get("model"),
|
|
2162
|
+
"max_tokens_cap": sampling.get("maxTokensCap") or sampling.get("max_tokens_cap"),
|
|
2163
|
+
"timeout": sampling.get("timeout"),
|
|
2164
|
+
"max_rpm": sampling.get("maxRpm") or sampling.get("max_rpm"),
|
|
2165
|
+
}.items() if v is not None
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
existing_mcp[name] = hermes_srv
|
|
2169
|
+
added += 1
|
|
2170
|
+
self.record("mcp-servers", f"mcp.servers.{name}", f"config.yaml mcp_servers.{name}",
|
|
2171
|
+
"migrated", servers_added=added)
|
|
2172
|
+
|
|
2173
|
+
if added > 0 and self.execute:
|
|
2174
|
+
self.maybe_backup(hermes_cfg_path)
|
|
2175
|
+
hermes_cfg["mcp_servers"] = existing_mcp
|
|
2176
|
+
dump_yaml_file(hermes_cfg_path, hermes_cfg)
|
|
2177
|
+
|
|
2178
|
+
# ── 插件 ───────────────────────────────────────────────
|
|
2179
|
+
def migrate_plugins_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2180
|
+
config = config or self.load_openclaw_config()
|
|
2181
|
+
plugins = config.get("plugins") or {}
|
|
2182
|
+
if not plugins:
|
|
2183
|
+
self.record("plugins-config", None, None, "skipped", "No plugins configuration found")
|
|
2184
|
+
return
|
|
2185
|
+
|
|
2186
|
+
# 归档完整的插件配置
|
|
2187
|
+
if self.archive_dir and self.execute:
|
|
2188
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2189
|
+
dest = self.archive_dir / "plugins-config.json"
|
|
2190
|
+
dest.write_text(json.dumps(plugins, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2191
|
+
self.record("plugins-config", "openclaw.json plugins.*", str(dest), "archived",
|
|
2192
|
+
"Plugins config archived for manual review")
|
|
2193
|
+
else:
|
|
2194
|
+
self.record("plugins-config", "openclaw.json plugins.*", "archive/plugins-config.json",
|
|
2195
|
+
"archived" if not self.execute else "migrated", "Would archive plugins config")
|
|
2196
|
+
|
|
2197
|
+
# 如果存在,则复制 extensions 目录
|
|
2198
|
+
ext_dir = self.source_root / "extensions"
|
|
2199
|
+
if ext_dir.is_dir() and self.archive_dir:
|
|
2200
|
+
dest_ext = self.archive_dir / "extensions"
|
|
2201
|
+
if self.execute:
|
|
2202
|
+
shutil.copytree(ext_dir, dest_ext, dirs_exist_ok=True)
|
|
2203
|
+
self.record("plugins-config", str(ext_dir), str(dest_ext), "archived",
|
|
2204
|
+
"Extensions directory archived")
|
|
2205
|
+
|
|
2206
|
+
# 提取所有插件环境变量
|
|
2207
|
+
entries = plugins.get("entries") or {}
|
|
2208
|
+
for plugin_name, plugin_cfg in entries.items():
|
|
2209
|
+
if isinstance(plugin_cfg, dict):
|
|
2210
|
+
env_vars = plugin_cfg.get("env") or {}
|
|
2211
|
+
api_key = plugin_cfg.get("apiKey")
|
|
2212
|
+
if api_key and self.migrate_secrets:
|
|
2213
|
+
env_key = f"PLUGIN_{plugin_name.upper().replace('-', '_')}_API_KEY"
|
|
2214
|
+
self._set_env_var(env_key, api_key, f"plugins.entries.{plugin_name}.apiKey")
|
|
2215
|
+
|
|
2216
|
+
# ── 定时任务 ─────────────────────────────────────────────
|
|
2217
|
+
def migrate_cron_jobs(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2218
|
+
config = config or self.load_openclaw_config()
|
|
2219
|
+
cron = config.get("cron") or {}
|
|
2220
|
+
cron_store = self.source_root / "cron"
|
|
2221
|
+
found_any = False
|
|
2222
|
+
|
|
2223
|
+
# 存在时,归档完整的 cron 配置
|
|
2224
|
+
if cron:
|
|
2225
|
+
found_any = True
|
|
2226
|
+
if self.archive_dir and self.execute:
|
|
2227
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2228
|
+
dest = self.archive_dir / "cron-config.json"
|
|
2229
|
+
dest.write_text(json.dumps(cron, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2230
|
+
self.record("cron-jobs", "openclaw.json cron.*", str(dest), "archived",
|
|
2231
|
+
"Cron config archived. Use 'hermes cron' to recreate jobs manually.")
|
|
2232
|
+
else:
|
|
2233
|
+
self.record("cron-jobs", "openclaw.json cron.*", "archive/cron-config.json",
|
|
2234
|
+
"archived", "Would archive cron config")
|
|
2235
|
+
|
|
2236
|
+
# 即使缺少 config.cron,也要检查 cron 存储文件
|
|
2237
|
+
if cron_store.is_dir() and self.archive_dir:
|
|
2238
|
+
found_any = True
|
|
2239
|
+
dest_cron = self.archive_dir / "cron-store"
|
|
2240
|
+
if self.execute:
|
|
2241
|
+
shutil.copytree(cron_store, dest_cron, dirs_exist_ok=True)
|
|
2242
|
+
self.record("cron-jobs", str(cron_store), str(dest_cron), "archived",
|
|
2243
|
+
"Cron job store archived")
|
|
2244
|
+
|
|
2245
|
+
if not found_any:
|
|
2246
|
+
self.record("cron-jobs", None, None, "skipped", "No cron configuration found")
|
|
2247
|
+
|
|
2248
|
+
# ── 钩子 ─────────────────────────────────────────────────
|
|
2249
|
+
def migrate_hooks_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2250
|
+
config = config or self.load_openclaw_config()
|
|
2251
|
+
hooks = config.get("hooks") or {}
|
|
2252
|
+
if not hooks:
|
|
2253
|
+
self.record("hooks-config", None, None, "skipped", "No hooks configuration found")
|
|
2254
|
+
return
|
|
2255
|
+
|
|
2256
|
+
# 归档完整的钩子配置
|
|
2257
|
+
if self.archive_dir and self.execute:
|
|
2258
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2259
|
+
dest = self.archive_dir / "hooks-config.json"
|
|
2260
|
+
dest.write_text(json.dumps(hooks, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2261
|
+
self.record("hooks-config", "openclaw.json hooks.*", str(dest), "archived",
|
|
2262
|
+
"Hooks config archived for manual review")
|
|
2263
|
+
else:
|
|
2264
|
+
self.record("hooks-config", "openclaw.json hooks.*", "archive/hooks-config.json",
|
|
2265
|
+
"archived", "Would archive hooks config")
|
|
2266
|
+
|
|
2267
|
+
# 复制工作区钩子目录
|
|
2268
|
+
for ws_name in ("workspace", "workspace.default"):
|
|
2269
|
+
hooks_dir = self.source_root / ws_name / "hooks"
|
|
2270
|
+
if hooks_dir.is_dir() and self.archive_dir:
|
|
2271
|
+
dest_hooks = self.archive_dir / "workspace-hooks"
|
|
2272
|
+
if self.execute:
|
|
2273
|
+
shutil.copytree(hooks_dir, dest_hooks, dirs_exist_ok=True)
|
|
2274
|
+
self.record("hooks-config", str(hooks_dir), str(dest_hooks), "archived",
|
|
2275
|
+
"Workspace hooks directory archived")
|
|
2276
|
+
break
|
|
2277
|
+
|
|
2278
|
+
# ── Agent 配置 ──────────────────────────────────────────
|
|
2279
|
+
def migrate_agent_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2280
|
+
config = config or self.load_openclaw_config()
|
|
2281
|
+
agents = config.get("agents") or {}
|
|
2282
|
+
defaults = agents.get("defaults") or {}
|
|
2283
|
+
agent_list = agents.get("list") or []
|
|
2284
|
+
|
|
2285
|
+
if not defaults and not agent_list:
|
|
2286
|
+
self.record("agent-config", None, None, "skipped", "No agent configuration found")
|
|
2287
|
+
return
|
|
2288
|
+
|
|
2289
|
+
hermes_cfg_path = self.target_root / "config.yaml"
|
|
2290
|
+
hermes_cfg = load_yaml_file(hermes_cfg_path)
|
|
2291
|
+
changes = False
|
|
2292
|
+
|
|
2293
|
+
# 映射 agent 默认值
|
|
2294
|
+
agent_cfg = hermes_cfg.get("agent") or {}
|
|
2295
|
+
if defaults.get("contextTokens"):
|
|
2296
|
+
# 无直接映射但有有用的上下文
|
|
2297
|
+
pass
|
|
2298
|
+
if defaults.get("timeoutSeconds"):
|
|
2299
|
+
agent_cfg["max_turns"] = min(defaults["timeoutSeconds"] // 10, 200)
|
|
2300
|
+
changes = True
|
|
2301
|
+
if defaults.get("verboseDefault"):
|
|
2302
|
+
agent_cfg["verbose"] = defaults["verboseDefault"]
|
|
2303
|
+
changes = True
|
|
2304
|
+
if defaults.get("thinkingDefault"):
|
|
2305
|
+
# 将 OpenClaw thinking 映射 -> Hermes reasoning_effort
|
|
2306
|
+
thinking = defaults["thinkingDefault"]
|
|
2307
|
+
if thinking in {"always", "high", "xhigh"}:
|
|
2308
|
+
agent_cfg["reasoning_effort"] = "high"
|
|
2309
|
+
elif thinking in {"auto", "medium", "adaptive"}:
|
|
2310
|
+
agent_cfg["reasoning_effort"] = "medium"
|
|
2311
|
+
elif thinking in {"off", "low", "none", "minimal"}:
|
|
2312
|
+
agent_cfg["reasoning_effort"] = "low"
|
|
2313
|
+
changes = True
|
|
2314
|
+
|
|
2315
|
+
# 将 compaction 映射 -> compression
|
|
2316
|
+
compaction = defaults.get("compaction") or {}
|
|
2317
|
+
if compaction:
|
|
2318
|
+
compression = hermes_cfg.get("compression") or {}
|
|
2319
|
+
if compaction.get("mode") == "off":
|
|
2320
|
+
compression["enabled"] = False
|
|
2321
|
+
else:
|
|
2322
|
+
compression["enabled"] = True
|
|
2323
|
+
if compaction.get("timeout"):
|
|
2324
|
+
pass # 无直接映射
|
|
2325
|
+
if compaction.get("model"):
|
|
2326
|
+
aux = hermes_cfg.setdefault("auxiliary", {})
|
|
2327
|
+
aux_comp = aux.setdefault("compression", {})
|
|
2328
|
+
aux_comp["model"] = compaction["model"]
|
|
2329
|
+
hermes_cfg["compression"] = compression
|
|
2330
|
+
changes = True
|
|
2331
|
+
|
|
2332
|
+
# 映射 humanDelay
|
|
2333
|
+
human_delay = defaults.get("humanDelay") or {}
|
|
2334
|
+
if human_delay:
|
|
2335
|
+
hd = hermes_cfg.get("human_delay") or {}
|
|
2336
|
+
hd_mode = human_delay.get("mode") or ("natural" if human_delay.get("enabled") else None)
|
|
2337
|
+
if hd_mode and hd_mode != "off":
|
|
2338
|
+
hd["mode"] = hd_mode
|
|
2339
|
+
if human_delay.get("minMs"):
|
|
2340
|
+
hd["min_ms"] = human_delay["minMs"]
|
|
2341
|
+
if human_delay.get("maxMs"):
|
|
2342
|
+
hd["max_ms"] = human_delay["maxMs"]
|
|
2343
|
+
hermes_cfg["human_delay"] = hd
|
|
2344
|
+
changes = True
|
|
2345
|
+
|
|
2346
|
+
# 映射 userTimezone
|
|
2347
|
+
if defaults.get("userTimezone"):
|
|
2348
|
+
hermes_cfg["timezone"] = defaults["userTimezone"]
|
|
2349
|
+
changes = True
|
|
2350
|
+
|
|
2351
|
+
# 映射 terminal/exec 设置
|
|
2352
|
+
exec_cfg = (config.get("tools") or {}).get("exec") or {}
|
|
2353
|
+
if exec_cfg:
|
|
2354
|
+
terminal_cfg = hermes_cfg.get("terminal") or {}
|
|
2355
|
+
if exec_cfg.get("timeoutSec") or exec_cfg.get("timeout"):
|
|
2356
|
+
terminal_cfg["timeout"] = exec_cfg.get("timeoutSec") or exec_cfg.get("timeout")
|
|
2357
|
+
changes = True
|
|
2358
|
+
hermes_cfg["terminal"] = terminal_cfg
|
|
2359
|
+
|
|
2360
|
+
# 将 sandbox 映射 -> terminal docker 设置
|
|
2361
|
+
sandbox = defaults.get("sandbox") or {}
|
|
2362
|
+
if sandbox and sandbox.get("backend") == "docker":
|
|
2363
|
+
terminal_cfg = hermes_cfg.get("terminal") or {}
|
|
2364
|
+
terminal_cfg["backend"] = "docker"
|
|
2365
|
+
if sandbox.get("docker", {}).get("image"):
|
|
2366
|
+
terminal_cfg["docker_image"] = sandbox["docker"]["image"]
|
|
2367
|
+
hermes_cfg["terminal"] = terminal_cfg
|
|
2368
|
+
changes = True
|
|
2369
|
+
|
|
2370
|
+
if changes:
|
|
2371
|
+
hermes_cfg["agent"] = agent_cfg
|
|
2372
|
+
if self.execute:
|
|
2373
|
+
self.maybe_backup(hermes_cfg_path)
|
|
2374
|
+
dump_yaml_file(hermes_cfg_path, hermes_cfg)
|
|
2375
|
+
self.record("agent-config", "openclaw.json agents.defaults", "config.yaml agent/compression/terminal",
|
|
2376
|
+
"migrated", "Agent defaults mapped to Hermes config")
|
|
2377
|
+
|
|
2378
|
+
# 归档多智能体列表
|
|
2379
|
+
if agent_list:
|
|
2380
|
+
if self.archive_dir and self.execute:
|
|
2381
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2382
|
+
dest = self.archive_dir / "agents-list.json"
|
|
2383
|
+
dest.write_text(json.dumps(agent_list, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2384
|
+
self.record("agent-config", "openclaw.json agents.list", "archive/agents-list.json",
|
|
2385
|
+
"archived", f"Multi-agent setup ({len(agent_list)} agents) archived for manual recreation")
|
|
2386
|
+
|
|
2387
|
+
# 归档 bindings
|
|
2388
|
+
bindings = config.get("bindings") or []
|
|
2389
|
+
if bindings:
|
|
2390
|
+
if self.archive_dir and self.execute:
|
|
2391
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2392
|
+
dest = self.archive_dir / "bindings.json"
|
|
2393
|
+
dest.write_text(json.dumps(bindings, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2394
|
+
self.record("agent-config", "openclaw.json bindings", "archive/bindings.json",
|
|
2395
|
+
"archived", f"Agent routing bindings ({len(bindings)} rules) archived")
|
|
2396
|
+
|
|
2397
|
+
# ── 网关配置 ────────────────────────────────────────
|
|
2398
|
+
def migrate_gateway_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2399
|
+
config = config or self.load_openclaw_config()
|
|
2400
|
+
gateway = config.get("gateway") or {}
|
|
2401
|
+
if not gateway:
|
|
2402
|
+
self.record("gateway-config", None, None, "skipped", "No gateway configuration found")
|
|
2403
|
+
return
|
|
2404
|
+
|
|
2405
|
+
# 归档完整的网关配置(复杂,包含许多设置)
|
|
2406
|
+
if self.archive_dir and self.execute:
|
|
2407
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2408
|
+
dest = self.archive_dir / "gateway-config.json"
|
|
2409
|
+
dest.write_text(json.dumps(gateway, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2410
|
+
self.record("gateway-config", "openclaw.json gateway.*", "archive/gateway-config.json",
|
|
2411
|
+
"archived", "Gateway config archived. Use 'hermes gateway' to configure.")
|
|
2412
|
+
|
|
2413
|
+
# 如果存在,将网关认证令牌提取到 .env
|
|
2414
|
+
auth = gateway.get("auth") or {}
|
|
2415
|
+
if auth.get("token") and self.migrate_secrets:
|
|
2416
|
+
self._set_env_var("HERMES_GATEWAY_TOKEN", auth["token"], "gateway.auth.token")
|
|
2417
|
+
|
|
2418
|
+
# ── 会话配置 ────────────────────────────────────────
|
|
2419
|
+
def migrate_session_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2420
|
+
config = config or self.load_openclaw_config()
|
|
2421
|
+
session = config.get("session") or {}
|
|
2422
|
+
if not session:
|
|
2423
|
+
self.record("session-config", None, None, "skipped", "No session configuration found")
|
|
2424
|
+
return
|
|
2425
|
+
|
|
2426
|
+
hermes_cfg_path = self.target_root / "config.yaml"
|
|
2427
|
+
hermes_cfg = load_yaml_file(hermes_cfg_path)
|
|
2428
|
+
sr = hermes_cfg.get("session_reset") or {}
|
|
2429
|
+
changes = False
|
|
2430
|
+
|
|
2431
|
+
# OpenClaw 使用 session.reset(结构化)和 session.resetTriggers(字符串数组)
|
|
2432
|
+
reset = session.get("reset") or {}
|
|
2433
|
+
reset_triggers = session.get("resetTriggers") or session.get("reset_triggers") or []
|
|
2434
|
+
|
|
2435
|
+
if reset:
|
|
2436
|
+
# 结构化的重置配置:包含 mode、atHour、idleMinutes
|
|
2437
|
+
mode = reset.get("mode", "")
|
|
2438
|
+
if mode == "daily":
|
|
2439
|
+
sr["mode"] = "daily"
|
|
2440
|
+
elif mode == "idle":
|
|
2441
|
+
sr["mode"] = "idle"
|
|
2442
|
+
else:
|
|
2443
|
+
sr["mode"] = mode or "none"
|
|
2444
|
+
if reset.get("atHour") is not None:
|
|
2445
|
+
sr["at_hour"] = reset["atHour"]
|
|
2446
|
+
if reset.get("idleMinutes"):
|
|
2447
|
+
sr["idle_minutes"] = reset["idleMinutes"]
|
|
2448
|
+
changes = True
|
|
2449
|
+
elif isinstance(reset_triggers, list) and reset_triggers:
|
|
2450
|
+
# 简单的字符串触发器:["daily", "idle"]
|
|
2451
|
+
has_daily = "daily" in reset_triggers
|
|
2452
|
+
has_idle = "idle" in reset_triggers
|
|
2453
|
+
if has_daily and has_idle:
|
|
2454
|
+
sr["mode"] = "both"
|
|
2455
|
+
elif has_daily:
|
|
2456
|
+
sr["mode"] = "daily"
|
|
2457
|
+
elif has_idle:
|
|
2458
|
+
sr["mode"] = "idle"
|
|
2459
|
+
changes = True
|
|
2460
|
+
|
|
2461
|
+
if changes:
|
|
2462
|
+
hermes_cfg["session_reset"] = sr
|
|
2463
|
+
if self.execute:
|
|
2464
|
+
self.maybe_backup(hermes_cfg_path)
|
|
2465
|
+
dump_yaml_file(hermes_cfg_path, hermes_cfg)
|
|
2466
|
+
self.record("session-config", "openclaw.json session.resetTriggers",
|
|
2467
|
+
"config.yaml session_reset", "migrated")
|
|
2468
|
+
|
|
2469
|
+
# 归档完整的会话配置(身份链接、线程绑定等)
|
|
2470
|
+
complex_keys = {"identityLinks", "threadBindings", "maintenance", "scope", "sendPolicy"}
|
|
2471
|
+
complex_session = {k: v for k, v in session.items() if k in complex_keys and v}
|
|
2472
|
+
if complex_session and self.archive_dir:
|
|
2473
|
+
if self.execute:
|
|
2474
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2475
|
+
dest = self.archive_dir / "session-config.json"
|
|
2476
|
+
dest.write_text(json.dumps(complex_session, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2477
|
+
self.record("session-config", "openclaw.json session (advanced)",
|
|
2478
|
+
"archive/session-config.json", "archived",
|
|
2479
|
+
"Advanced session settings archived (identity links, thread bindings, etc.)")
|
|
2480
|
+
|
|
2481
|
+
# ── 完整的模型提供商 ──────────────────────────────────
|
|
2482
|
+
def migrate_full_providers(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2483
|
+
config = config or self.load_openclaw_config()
|
|
2484
|
+
models = config.get("models") or {}
|
|
2485
|
+
providers = models.get("providers") or {}
|
|
2486
|
+
if not providers:
|
|
2487
|
+
self.record("full-providers", None, None, "skipped", "No model providers found")
|
|
2488
|
+
return
|
|
2489
|
+
|
|
2490
|
+
hermes_cfg_path = self.target_root / "config.yaml"
|
|
2491
|
+
hermes_cfg = load_yaml_file(hermes_cfg_path)
|
|
2492
|
+
custom_providers = hermes_cfg.get("custom_providers") or []
|
|
2493
|
+
added = 0
|
|
2494
|
+
|
|
2495
|
+
# 知名提供商:只需提取 API 密钥
|
|
2496
|
+
WELL_KNOWN = {"openrouter", "openai", "anthropic", "deepseek", "google", "groq"}
|
|
2497
|
+
|
|
2498
|
+
for prov_name, prov_cfg in providers.items():
|
|
2499
|
+
if not isinstance(prov_cfg, dict):
|
|
2500
|
+
continue
|
|
2501
|
+
|
|
2502
|
+
# 提取 API 密钥到 .env
|
|
2503
|
+
api_key = prov_cfg.get("apiKey") or prov_cfg.get("api_key")
|
|
2504
|
+
if api_key and self.migrate_secrets:
|
|
2505
|
+
env_key = f"{prov_name.upper().replace('-', '_')}_API_KEY"
|
|
2506
|
+
self._set_env_var(env_key, api_key, f"models.providers.{prov_name}.apiKey")
|
|
2507
|
+
|
|
2508
|
+
# 对于非知名提供商,创建 custom_providers 条目
|
|
2509
|
+
if prov_name.lower() not in WELL_KNOWN and prov_cfg.get("baseUrl"):
|
|
2510
|
+
# 检查是否已存在
|
|
2511
|
+
existing_names = {p.get("name", "").lower() for p in custom_providers}
|
|
2512
|
+
if prov_name.lower() in existing_names and not self.overwrite:
|
|
2513
|
+
self.record("full-providers", f"models.providers.{prov_name}",
|
|
2514
|
+
"config.yaml custom_providers", "conflict",
|
|
2515
|
+
f"Provider '{prov_name}' already exists")
|
|
2516
|
+
continue
|
|
2517
|
+
|
|
2518
|
+
api_type = prov_cfg.get("apiType") or prov_cfg.get("api") or prov_cfg.get("type") or "openai"
|
|
2519
|
+
api_mode_map = {
|
|
2520
|
+
"openai": "chat_completions",
|
|
2521
|
+
"openai-completions": "chat_completions",
|
|
2522
|
+
"openai-responses": "chat_completions",
|
|
2523
|
+
"anthropic": "anthropic_messages",
|
|
2524
|
+
"anthropic-messages": "anthropic_messages",
|
|
2525
|
+
"google-generative-ai": "chat_completions",
|
|
2526
|
+
"cohere": "chat_completions",
|
|
2527
|
+
}
|
|
2528
|
+
entry = {
|
|
2529
|
+
"name": prov_name,
|
|
2530
|
+
"base_url": prov_cfg["baseUrl"],
|
|
2531
|
+
"api_key": "", # 从 .env 引用
|
|
2532
|
+
"api_mode": api_mode_map.get(api_type, "chat_completions"),
|
|
2533
|
+
}
|
|
2534
|
+
custom_providers.append(entry)
|
|
2535
|
+
added += 1
|
|
2536
|
+
self.record("full-providers", f"models.providers.{prov_name}",
|
|
2537
|
+
f"config.yaml custom_providers[{prov_name}]", "migrated")
|
|
2538
|
+
|
|
2539
|
+
if added > 0 and self.execute:
|
|
2540
|
+
self.maybe_backup(hermes_cfg_path)
|
|
2541
|
+
hermes_cfg["custom_providers"] = custom_providers
|
|
2542
|
+
dump_yaml_file(hermes_cfg_path, hermes_cfg)
|
|
2543
|
+
|
|
2544
|
+
# 归档模型别名/目录
|
|
2545
|
+
agent_defaults = (config.get("agents") or {}).get("defaults") or {}
|
|
2546
|
+
model_aliases = agent_defaults.get("models") or {}
|
|
2547
|
+
if model_aliases:
|
|
2548
|
+
if self.archive_dir and self.execute:
|
|
2549
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2550
|
+
dest = self.archive_dir / "model-aliases.json"
|
|
2551
|
+
dest.write_text(json.dumps(model_aliases, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2552
|
+
self.record("full-providers", "agents.defaults.models", "archive/model-aliases.json",
|
|
2553
|
+
"archived", f"Model aliases/catalog ({len(model_aliases)} entries) archived")
|
|
2554
|
+
|
|
2555
|
+
# ── 深度频道配置 ───────────────────────────────────
|
|
2556
|
+
def migrate_deep_channels(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2557
|
+
config = config or self.load_openclaw_config()
|
|
2558
|
+
channels = config.get("channels") or {}
|
|
2559
|
+
if not channels:
|
|
2560
|
+
self.record("deep-channels", None, None, "skipped", "No channel configuration found")
|
|
2561
|
+
return
|
|
2562
|
+
|
|
2563
|
+
# 扩展频道令牌/白名单映射
|
|
2564
|
+
CHANNEL_ENV_MAP = {
|
|
2565
|
+
"matrix": {"token": "MATRIX...OKEN", "tokenField": "accessToken", "allowFrom": "MATRIX_ALLOWED_USERS",
|
|
2566
|
+
"extras": {"homeserverUrl": "MATRIX_HOMESERVER_URL", "userId": "MATRIX_USER_ID"}},
|
|
2567
|
+
"mattermost": {"token": "MATTERMOST_BOT_TOKEN", "allowFrom": "MATTERMOST_ALLOWED_USERS",
|
|
2568
|
+
"extras": {"url": "MATTERMOST_URL", "teamId": "MATTERMOST_TEAM_ID"}},
|
|
2569
|
+
"irc": {"extras": {"server": "IRC_SERVER", "nick": "IRC_NICK", "channels": "IRC_CHANNELS"}},
|
|
2570
|
+
"googlechat": {"extras": {"serviceAccountKeyPath": "GOOGLE_CHAT_SA_KEY_PATH"}},
|
|
2571
|
+
"imessage": {},
|
|
2572
|
+
"bluebubbles": {"extras": {"server": "BLUEBUBBLES_SERVER", "password": "BLUEBUBBLES_PASSWORD"}},
|
|
2573
|
+
"msteams": {"token": "MSTEAMS_BOT_TOKEN", "allowFrom": "MSTEAMS_ALLOWED_USERS"},
|
|
2574
|
+
"nostr": {"extras": {"nsec": "NOSTR_NSEC", "relays": "NOSTR_RELAYS"}},
|
|
2575
|
+
"twitch": {"token": "TWITCH_BOT_TOKEN", "extras": {"channels": "TWITCH_CHANNELS"}},
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
for ch_name, ch_mapping in CHANNEL_ENV_MAP.items():
|
|
2579
|
+
ch_cfg = channels.get(ch_name) or {}
|
|
2580
|
+
if not ch_cfg:
|
|
2581
|
+
continue
|
|
2582
|
+
|
|
2583
|
+
# 提取令牌(检查扁平路径,然后是 accounts.default)
|
|
2584
|
+
token_field = ch_mapping.get("tokenField", "botToken")
|
|
2585
|
+
bot_token = self._get_channel_field(ch_cfg, token_field)
|
|
2586
|
+
if ch_mapping.get("token") and bot_token and self.migrate_secrets:
|
|
2587
|
+
self._set_env_var(ch_mapping["token"], str(bot_token),
|
|
2588
|
+
f"channels.{ch_name}.{token_field}")
|
|
2589
|
+
allow_val = self._get_channel_field(ch_cfg, "allowFrom")
|
|
2590
|
+
if ch_mapping.get("allowFrom") and allow_val:
|
|
2591
|
+
if isinstance(allow_val, list):
|
|
2592
|
+
allow_val = ",".join(str(x) for x in allow_val)
|
|
2593
|
+
self._set_env_var(ch_mapping["allowFrom"], str(allow_val),
|
|
2594
|
+
f"channels.{ch_name}.allowFrom")
|
|
2595
|
+
# 额外字段
|
|
2596
|
+
for oc_key, env_key in (ch_mapping.get("extras") or {}).items():
|
|
2597
|
+
val = self._get_channel_field(ch_cfg, oc_key)
|
|
2598
|
+
if val:
|
|
2599
|
+
if isinstance(val, list):
|
|
2600
|
+
val = ",".join(str(x) for x in val)
|
|
2601
|
+
is_secret = "password" in oc_key.lower() or "token" in oc_key.lower() or "nsec" in oc_key.lower()
|
|
2602
|
+
if is_secret and not self.migrate_secrets:
|
|
2603
|
+
continue
|
|
2604
|
+
self._set_env_var(env_key, str(val), f"channels.{ch_name}.{oc_key}")
|
|
2605
|
+
|
|
2606
|
+
# 将 Discord 特定设置映射到 Hermes 配置
|
|
2607
|
+
discord_cfg = channels.get("discord") or {}
|
|
2608
|
+
if discord_cfg:
|
|
2609
|
+
hermes_cfg_path = self.target_root / "config.yaml"
|
|
2610
|
+
hermes_cfg = load_yaml_file(hermes_cfg_path)
|
|
2611
|
+
discord_hermes = hermes_cfg.get("discord") or {}
|
|
2612
|
+
changed = False
|
|
2613
|
+
if "requireMention" in discord_cfg:
|
|
2614
|
+
discord_hermes["require_mention"] = discord_cfg["requireMention"]
|
|
2615
|
+
changed = True
|
|
2616
|
+
if discord_cfg.get("autoThread") is not None:
|
|
2617
|
+
discord_hermes["auto_thread"] = discord_cfg["autoThread"]
|
|
2618
|
+
changed = True
|
|
2619
|
+
if changed and self.execute:
|
|
2620
|
+
hermes_cfg["discord"] = discord_hermes
|
|
2621
|
+
dump_yaml_file(hermes_cfg_path, hermes_cfg)
|
|
2622
|
+
|
|
2623
|
+
# 归档复杂的频道配置(群组设置、线程绑定等)
|
|
2624
|
+
complex_archive = {}
|
|
2625
|
+
for ch_name, ch_cfg in channels.items():
|
|
2626
|
+
if not isinstance(ch_cfg, dict):
|
|
2627
|
+
continue
|
|
2628
|
+
complex_keys = {k: v for k, v in ch_cfg.items()
|
|
2629
|
+
if k not in {"botToken", "appToken", "allowFrom", "enabled"}
|
|
2630
|
+
and v and k not in {"requireMention", "autoThread"}}
|
|
2631
|
+
if complex_keys:
|
|
2632
|
+
complex_archive[ch_name] = complex_keys
|
|
2633
|
+
|
|
2634
|
+
if complex_archive and self.archive_dir:
|
|
2635
|
+
if self.execute:
|
|
2636
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2637
|
+
dest = self.archive_dir / "channels-deep-config.json"
|
|
2638
|
+
dest.write_text(json.dumps(complex_archive, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2639
|
+
self.record("deep-channels", "openclaw.json channels (advanced settings)",
|
|
2640
|
+
"archive/channels-deep-config.json", "archived",
|
|
2641
|
+
f"Deep channel config for {len(complex_archive)} channels archived")
|
|
2642
|
+
|
|
2643
|
+
# ── 浏览器配置 ────────────────────────────────────────
|
|
2644
|
+
def migrate_browser_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2645
|
+
config = config or self.load_openclaw_config()
|
|
2646
|
+
browser = config.get("browser") or {}
|
|
2647
|
+
if not browser:
|
|
2648
|
+
self.record("browser-config", None, None, "skipped", "No browser configuration found")
|
|
2649
|
+
return
|
|
2650
|
+
|
|
2651
|
+
hermes_cfg_path = self.target_root / "config.yaml"
|
|
2652
|
+
hermes_cfg = load_yaml_file(hermes_cfg_path)
|
|
2653
|
+
browser_hermes = hermes_cfg.get("browser") or {}
|
|
2654
|
+
changed = False
|
|
2655
|
+
|
|
2656
|
+
# 映射具有 Hermes 等价物的字段
|
|
2657
|
+
if browser.get("cdpUrl"):
|
|
2658
|
+
browser_hermes["cdp_url"] = browser["cdpUrl"]
|
|
2659
|
+
changed = True
|
|
2660
|
+
if browser.get("headless") is not None:
|
|
2661
|
+
browser_hermes["headless"] = browser["headless"]
|
|
2662
|
+
changed = True
|
|
2663
|
+
|
|
2664
|
+
if changed:
|
|
2665
|
+
hermes_cfg["browser"] = browser_hermes
|
|
2666
|
+
if self.execute:
|
|
2667
|
+
self.maybe_backup(hermes_cfg_path)
|
|
2668
|
+
dump_yaml_file(hermes_cfg_path, hermes_cfg)
|
|
2669
|
+
self.record("browser-config", "openclaw.json browser.*", "config.yaml browser",
|
|
2670
|
+
"migrated")
|
|
2671
|
+
|
|
2672
|
+
# 归档剩余的浏览器设置
|
|
2673
|
+
advanced = {k: v for k, v in browser.items()
|
|
2674
|
+
if k not in {"cdpUrl", "headless"} and v}
|
|
2675
|
+
if advanced and self.archive_dir:
|
|
2676
|
+
if self.execute:
|
|
2677
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2678
|
+
dest = self.archive_dir / "browser-config.json"
|
|
2679
|
+
dest.write_text(json.dumps(advanced, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2680
|
+
self.record("browser-config", "openclaw.json browser (advanced)",
|
|
2681
|
+
"archive/browser-config.json", "archived")
|
|
2682
|
+
|
|
2683
|
+
# ── 工具配置 ──────────────────────────────────────────
|
|
2684
|
+
def migrate_tools_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2685
|
+
config = config or self.load_openclaw_config()
|
|
2686
|
+
tools = config.get("tools") or {}
|
|
2687
|
+
if not tools:
|
|
2688
|
+
self.record("tools-config", None, None, "skipped", "No tools configuration found")
|
|
2689
|
+
return
|
|
2690
|
+
|
|
2691
|
+
hermes_cfg_path = self.target_root / "config.yaml"
|
|
2692
|
+
hermes_cfg = load_yaml_file(hermes_cfg_path)
|
|
2693
|
+
changed = False
|
|
2694
|
+
|
|
2695
|
+
# 将 exec timeout 映射 -> terminal timeout(在 OpenClaw 中字段为 timeoutSec)
|
|
2696
|
+
exec_cfg = tools.get("exec") or {}
|
|
2697
|
+
timeout_val = exec_cfg.get("timeoutSec") or exec_cfg.get("timeout")
|
|
2698
|
+
if timeout_val:
|
|
2699
|
+
terminal_cfg = hermes_cfg.get("terminal") or {}
|
|
2700
|
+
terminal_cfg["timeout"] = timeout_val
|
|
2701
|
+
hermes_cfg["terminal"] = terminal_cfg
|
|
2702
|
+
changed = True
|
|
2703
|
+
|
|
2704
|
+
# 映射网络搜索 API 密钥(在 OpenClaw 中路径为 tools.web.search.brave.apiKey)
|
|
2705
|
+
web_cfg = tools.get("web") or tools.get("webSearch") or {}
|
|
2706
|
+
search_cfg = web_cfg.get("search") or web_cfg if not web_cfg.get("search") else web_cfg["search"]
|
|
2707
|
+
brave_cfg = search_cfg.get("brave") or {}
|
|
2708
|
+
brave_key = brave_cfg.get("apiKey") or search_cfg.get("braveApiKey") or web_cfg.get("braveApiKey")
|
|
2709
|
+
if brave_key and isinstance(brave_key, str) and self.migrate_secrets:
|
|
2710
|
+
self._set_env_var("BRAVE_API_KEY", brave_key, "tools.web.search.brave.apiKey")
|
|
2711
|
+
|
|
2712
|
+
if changed and self.execute:
|
|
2713
|
+
self.maybe_backup(hermes_cfg_path)
|
|
2714
|
+
dump_yaml_file(hermes_cfg_path, hermes_cfg)
|
|
2715
|
+
self.record("tools-config", "openclaw.json tools.*", "config.yaml terminal",
|
|
2716
|
+
"migrated")
|
|
2717
|
+
|
|
2718
|
+
# 归档完整的工具配置
|
|
2719
|
+
if self.archive_dir:
|
|
2720
|
+
if self.execute:
|
|
2721
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2722
|
+
dest = self.archive_dir / "tools-config.json"
|
|
2723
|
+
dest.write_text(json.dumps(tools, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2724
|
+
self.record("tools-config", "openclaw.json tools (full)", "archive/tools-config.json",
|
|
2725
|
+
"archived", "Full tools config archived for reference")
|
|
2726
|
+
|
|
2727
|
+
# ── 审批配置 ──────────────────────────────────────
|
|
2728
|
+
def migrate_approvals_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2729
|
+
config = config or self.load_openclaw_config()
|
|
2730
|
+
approvals = config.get("approvals") or {}
|
|
2731
|
+
if not approvals:
|
|
2732
|
+
self.record("approvals-config", None, None, "skipped", "No approvals configuration found")
|
|
2733
|
+
return
|
|
2734
|
+
|
|
2735
|
+
hermes_cfg_path = self.target_root / "config.yaml"
|
|
2736
|
+
hermes_cfg = load_yaml_file(hermes_cfg_path)
|
|
2737
|
+
|
|
2738
|
+
# 映射审批模式(在 OpenClaw 中嵌套在 approvals.exec.mode 下)
|
|
2739
|
+
exec_approvals = approvals.get("exec") or {}
|
|
2740
|
+
mode = (exec_approvals.get("mode") if isinstance(exec_approvals, dict) else None) or approvals.get("mode") or approvals.get("defaultMode")
|
|
2741
|
+
if mode:
|
|
2742
|
+
mode_map = {"auto": "off", "always": "manual", "smart": "smart", "manual": "manual"}
|
|
2743
|
+
hermes_mode = mode_map.get(mode, "manual")
|
|
2744
|
+
hermes_cfg.setdefault("approvals", {})["mode"] = hermes_mode
|
|
2745
|
+
if self.execute:
|
|
2746
|
+
self.maybe_backup(hermes_cfg_path)
|
|
2747
|
+
dump_yaml_file(hermes_cfg_path, hermes_cfg)
|
|
2748
|
+
self.record("approvals-config", "openclaw.json approvals.mode",
|
|
2749
|
+
"config.yaml approvals.mode", "migrated", f"Mapped '{mode}' -> '{hermes_mode}'")
|
|
2750
|
+
|
|
2751
|
+
# 归档完整审批配置
|
|
2752
|
+
if len(approvals) > 1 and self.archive_dir:
|
|
2753
|
+
if self.execute:
|
|
2754
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2755
|
+
dest = self.archive_dir / "approvals-config.json"
|
|
2756
|
+
dest.write_text(json.dumps(approvals, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2757
|
+
self.record("approvals-config", "openclaw.json approvals (rules)",
|
|
2758
|
+
"archive/approvals-config.json", "archived")
|
|
2759
|
+
|
|
2760
|
+
# ── 内存后端 ────────────────────────────────────────
|
|
2761
|
+
def migrate_memory_backend(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2762
|
+
config = config or self.load_openclaw_config()
|
|
2763
|
+
memory = config.get("memory") or {}
|
|
2764
|
+
if not memory:
|
|
2765
|
+
self.record("memory-backend", None, None, "skipped", "No memory backend configuration found")
|
|
2766
|
+
return
|
|
2767
|
+
|
|
2768
|
+
if self.archive_dir and self.execute:
|
|
2769
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2770
|
+
dest = self.archive_dir / "memory-backend-config.json"
|
|
2771
|
+
dest.write_text(json.dumps(memory, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2772
|
+
self.record("memory-backend", "openclaw.json memory.*", "archive/memory-backend-config.json",
|
|
2773
|
+
"archived", "Memory backend config (QMD, vector search, citations) archived for manual review")
|
|
2774
|
+
|
|
2775
|
+
# ── 技能配置 ─────────────────────────────────────────
|
|
2776
|
+
def migrate_skills_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2777
|
+
config = config or self.load_openclaw_config()
|
|
2778
|
+
skills = config.get("skills") or {}
|
|
2779
|
+
entries = skills.get("entries") or {}
|
|
2780
|
+
if not entries and not skills:
|
|
2781
|
+
self.record("skills-config", None, None, "skipped", "No skills registry configuration found")
|
|
2782
|
+
return
|
|
2783
|
+
|
|
2784
|
+
if self.archive_dir and self.execute:
|
|
2785
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2786
|
+
dest = self.archive_dir / "skills-registry-config.json"
|
|
2787
|
+
dest.write_text(json.dumps(skills, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2788
|
+
self.record("skills-config", "openclaw.json skills.*", "archive/skills-registry-config.json",
|
|
2789
|
+
"archived", f"Skills registry config ({len(entries)} entries) archived")
|
|
2790
|
+
|
|
2791
|
+
# ── UI / 身份 ─────────────────────────────────────────
|
|
2792
|
+
def migrate_ui_identity(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2793
|
+
config = config or self.load_openclaw_config()
|
|
2794
|
+
ui = config.get("ui") or {}
|
|
2795
|
+
if not ui:
|
|
2796
|
+
self.record("ui-identity", None, None, "skipped", "No UI/identity configuration found")
|
|
2797
|
+
return
|
|
2798
|
+
|
|
2799
|
+
if self.archive_dir and self.execute:
|
|
2800
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2801
|
+
dest = self.archive_dir / "ui-identity-config.json"
|
|
2802
|
+
dest.write_text(json.dumps(ui, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2803
|
+
self.record("ui-identity", "openclaw.json ui.*", "archive/ui-identity-config.json",
|
|
2804
|
+
"archived", "UI theme and identity settings archived")
|
|
2805
|
+
|
|
2806
|
+
# ── 日志 / 诊断 ─────────────────────────────────────────
|
|
2807
|
+
def migrate_logging_config(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
2808
|
+
config = config or self.load_openclaw_config()
|
|
2809
|
+
logging_cfg = config.get("logging") or {}
|
|
2810
|
+
diagnostics = config.get("diagnostics") or {}
|
|
2811
|
+
combined = {}
|
|
2812
|
+
if logging_cfg:
|
|
2813
|
+
combined["logging"] = logging_cfg
|
|
2814
|
+
if diagnostics:
|
|
2815
|
+
combined["diagnostics"] = diagnostics
|
|
2816
|
+
if not combined:
|
|
2817
|
+
self.record("logging-config", None, None, "skipped", "No logging/diagnostics configuration found")
|
|
2818
|
+
return
|
|
2819
|
+
|
|
2820
|
+
if self.archive_dir and self.execute:
|
|
2821
|
+
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
2822
|
+
dest = self.archive_dir / "logging-diagnostics-config.json"
|
|
2823
|
+
dest.write_text(json.dumps(combined, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2824
|
+
self.record("logging-config", "openclaw.json logging/diagnostics",
|
|
2825
|
+
"archive/logging-diagnostics-config.json", "archived")
|
|
2826
|
+
|
|
2827
|
+
# ── 辅助函数:设置环境变量 ───────────────────────────
|
|
2828
|
+
def _set_env_var(self, key: str, value: str, source_label: str) -> None:
|
|
2829
|
+
env_path = self.target_root / ".env"
|
|
2830
|
+
if self.execute:
|
|
2831
|
+
env_data = parse_env_file(env_path)
|
|
2832
|
+
if key in env_data and not self.overwrite:
|
|
2833
|
+
self.record("env-var", source_label, f".env {key}", "conflict",
|
|
2834
|
+
f"Env var {key} already set")
|
|
2835
|
+
return
|
|
2836
|
+
env_data[key] = value
|
|
2837
|
+
save_env_file(env_path, env_data)
|
|
2838
|
+
self.record("env-var", source_label, f".env {key}", "migrated")
|
|
2839
|
+
|
|
2840
|
+
# ── 生成迁移说明 ──────────────────────────────────────
|
|
2841
|
+
def generate_migration_notes(self) -> None:
|
|
2842
|
+
if not self.output_dir:
|
|
2843
|
+
return
|
|
2844
|
+
notes = [
|
|
2845
|
+
"# OpenClaw -> Hermes Migration Notes",
|
|
2846
|
+
"",
|
|
2847
|
+
"This document lists items that require manual attention after migration.",
|
|
2848
|
+
"",
|
|
2849
|
+
"## PM2 / External Processes",
|
|
2850
|
+
"",
|
|
2851
|
+
"Your PM2 processes (Discord bots, Telegram bots, etc.) are NOT affected",
|
|
2852
|
+
"by this migration. They run independently and will continue working.",
|
|
2853
|
+
"No action needed for PM2-managed processes.",
|
|
2854
|
+
"",
|
|
2855
|
+
]
|
|
2856
|
+
|
|
2857
|
+
archived = [i for i in self.items if i.status == "archived"]
|
|
2858
|
+
if archived:
|
|
2859
|
+
notes.extend([
|
|
2860
|
+
"## Archived Items (Manual Review Needed)",
|
|
2861
|
+
"",
|
|
2862
|
+
"These OpenClaw configurations were archived because they don't have a",
|
|
2863
|
+
"direct 1:1 mapping in Hermes. Review each file and recreate manually:",
|
|
2864
|
+
"",
|
|
2865
|
+
])
|
|
2866
|
+
for item in archived:
|
|
2867
|
+
notes.append(f"- **{item.kind}**: `{item.destination}` -- {item.reason}")
|
|
2868
|
+
notes.append("")
|
|
2869
|
+
|
|
2870
|
+
conflicts = [i for i in self.items if i.status == "conflict"]
|
|
2871
|
+
if conflicts:
|
|
2872
|
+
notes.extend([
|
|
2873
|
+
"## Conflicts (Existing Hermes Config Not Overwritten)",
|
|
2874
|
+
"",
|
|
2875
|
+
"These items already existed in your Hermes config. Re-run with",
|
|
2876
|
+
"`--overwrite` to force, or merge manually:",
|
|
2877
|
+
"",
|
|
2878
|
+
])
|
|
2879
|
+
for item in conflicts:
|
|
2880
|
+
notes.append(f"- **{item.kind}**: {item.reason}")
|
|
2881
|
+
notes.append("")
|
|
2882
|
+
|
|
2883
|
+
has_cron_config_archive = any(
|
|
2884
|
+
i.kind == "cron-jobs" and i.status == "archived" and i.destination and i.destination.endswith("cron-config.json")
|
|
2885
|
+
for i in self.items
|
|
2886
|
+
)
|
|
2887
|
+
has_cron_store_archive = any(
|
|
2888
|
+
i.kind == "cron-jobs" and i.status == "archived" and i.destination and i.destination.endswith("cron-store")
|
|
2889
|
+
for i in self.items
|
|
2890
|
+
)
|
|
2891
|
+
|
|
2892
|
+
notes.extend([
|
|
2893
|
+
"## IMPORTANT: Archive the OpenClaw Directory",
|
|
2894
|
+
"",
|
|
2895
|
+
"After migration, your OpenClaw directory still exists on disk with workspace",
|
|
2896
|
+
"state files (todo.json, sessions, logs). If the Hermes agent discovers these",
|
|
2897
|
+
"directories, it may read/write to them instead of the Hermes state, causing",
|
|
2898
|
+
"confusion (e.g., cron jobs reading a different todo list than interactive sessions).",
|
|
2899
|
+
"",
|
|
2900
|
+
"**Strongly recommended:** Run `hermes claw cleanup` to rename the OpenClaw",
|
|
2901
|
+
"directory to `.openclaw.pre-migration`. This prevents the agent from finding it.",
|
|
2902
|
+
"The directory is renamed, not deleted — you can undo this at any time.",
|
|
2903
|
+
"",
|
|
2904
|
+
"If you skip this step and notice the agent getting confused about workspaces",
|
|
2905
|
+
"or todo lists, run `hermes claw cleanup` to fix it.",
|
|
2906
|
+
"",
|
|
2907
|
+
"## Hermes-Specific Setup",
|
|
2908
|
+
"",
|
|
2909
|
+
"After migration, you may want to:",
|
|
2910
|
+
"- Run `hermes claw cleanup` to archive the OpenClaw directory (prevents state confusion)",
|
|
2911
|
+
"- Run `hermes setup` to configure any remaining settings",
|
|
2912
|
+
"- Run `hermes mcp list` to verify MCP servers were imported correctly",
|
|
2913
|
+
])
|
|
2914
|
+
|
|
2915
|
+
if has_cron_config_archive:
|
|
2916
|
+
notes.append("- Run `hermes cron` to recreate scheduled tasks (see archive/cron-config.json)")
|
|
2917
|
+
elif has_cron_store_archive:
|
|
2918
|
+
notes.append("- Run `hermes cron` to recreate scheduled tasks (see archived cron-store)")
|
|
2919
|
+
|
|
2920
|
+
# 检查是否导入了技能
|
|
2921
|
+
has_skills = any(i.kind == "skills" and i.status == "migrated" for i in self.items)
|
|
2922
|
+
if has_skills:
|
|
2923
|
+
notes.extend([
|
|
2924
|
+
"",
|
|
2925
|
+
"## Imported Skills",
|
|
2926
|
+
"",
|
|
2927
|
+
"Imported skills require a new session to take effect. After migration,",
|
|
2928
|
+
"restart your agent or start a new chat session, then run `/skills`",
|
|
2929
|
+
"to verify they loaded correctly.",
|
|
2930
|
+
"",
|
|
2931
|
+
])
|
|
2932
|
+
|
|
2933
|
+
# 检查是否检测到 WhatsApp
|
|
2934
|
+
has_whatsapp = any(i.kind == "whatsapp-settings" and i.status == "migrated" for i in self.items)
|
|
2935
|
+
if has_whatsapp:
|
|
2936
|
+
notes.extend([
|
|
2937
|
+
"",
|
|
2938
|
+
"## WhatsApp Requires Re-Pairing",
|
|
2939
|
+
"",
|
|
2940
|
+
"WhatsApp uses QR-code pairing, not token-based auth. Your allowlist",
|
|
2941
|
+
"was migrated, but you must re-pair the device by running:",
|
|
2942
|
+
"",
|
|
2943
|
+
" hermes whatsapp",
|
|
2944
|
+
"",
|
|
2945
|
+
])
|
|
2946
|
+
|
|
2947
|
+
notes.extend([
|
|
2948
|
+
"- Run `hermes gateway install` if you need the gateway service",
|
|
2949
|
+
"- Review `~/.hermes/config.yaml` for any adjustments",
|
|
2950
|
+
"",
|
|
2951
|
+
])
|
|
2952
|
+
|
|
2953
|
+
if self.execute:
|
|
2954
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
2955
|
+
(self.output_dir / "MIGRATION_NOTES.md").write_text(
|
|
2956
|
+
"\n".join(notes) + "\n", encoding="utf-8"
|
|
2957
|
+
)
|
|
2958
|
+
|
|
2959
|
+
|
|
2960
|
+
def parse_args() -> argparse.Namespace:
|
|
2961
|
+
parser = argparse.ArgumentParser(description="Migrate OpenClaw user state into Hermes Agent.")
|
|
2962
|
+
parser.add_argument("--source", default=str(Path.home() / ".openclaw"), help="OpenClaw home directory")
|
|
2963
|
+
parser.add_argument("--target", default=os.environ.get("HERMES_HOME") or str(Path.home() / ".hermes"), help="Hermes home directory")
|
|
2964
|
+
parser.add_argument(
|
|
2965
|
+
"--workspace-target",
|
|
2966
|
+
help="Optional workspace root where the workspace instructions file should be copied",
|
|
2967
|
+
)
|
|
2968
|
+
parser.add_argument("--execute", action="store_true", help="Apply changes instead of reporting a dry run")
|
|
2969
|
+
parser.add_argument("--overwrite", action="store_true", help="Overwrite existing Hermes targets after backing them up")
|
|
2970
|
+
parser.add_argument(
|
|
2971
|
+
"--migrate-secrets",
|
|
2972
|
+
action="store_true",
|
|
2973
|
+
help="Import a narrow allowlist of Hermes-compatible secrets into the target env file",
|
|
2974
|
+
)
|
|
2975
|
+
parser.add_argument(
|
|
2976
|
+
"--skill-conflict",
|
|
2977
|
+
choices=sorted(SKILL_CONFLICT_MODES),
|
|
2978
|
+
default="skip",
|
|
2979
|
+
help="How to handle imported skill directory conflicts: skip, overwrite, or rename the imported copy.",
|
|
2980
|
+
)
|
|
2981
|
+
parser.add_argument(
|
|
2982
|
+
"--preset",
|
|
2983
|
+
choices=sorted(MIGRATION_PRESETS),
|
|
2984
|
+
help="Apply a named migration preset. 'user-data' excludes allowlisted secrets; 'full' includes all compatible groups.",
|
|
2985
|
+
)
|
|
2986
|
+
parser.add_argument(
|
|
2987
|
+
"--include",
|
|
2988
|
+
action="append",
|
|
2989
|
+
default=[],
|
|
2990
|
+
help="Comma-separated migration option ids to include (default: all). "
|
|
2991
|
+
f"Valid ids: {', '.join(sorted(MIGRATION_OPTION_METADATA))}",
|
|
2992
|
+
)
|
|
2993
|
+
parser.add_argument(
|
|
2994
|
+
"--exclude",
|
|
2995
|
+
action="append",
|
|
2996
|
+
default=[],
|
|
2997
|
+
help="Comma-separated migration option ids to skip. "
|
|
2998
|
+
f"Valid ids: {', '.join(sorted(MIGRATION_OPTION_METADATA))}",
|
|
2999
|
+
)
|
|
3000
|
+
parser.add_argument("--output-dir", help="Where to write report, backups, and archived docs")
|
|
3001
|
+
parser.add_argument(
|
|
3002
|
+
"--json",
|
|
3003
|
+
action="store_true",
|
|
3004
|
+
dest="json_output",
|
|
3005
|
+
help="Print the migration report as JSON on stdout (redacted). "
|
|
3006
|
+
"Combine with no --execute for a safe plan-only machine-readable preview.",
|
|
3007
|
+
)
|
|
3008
|
+
return parser.parse_args()
|
|
3009
|
+
|
|
3010
|
+
|
|
3011
|
+
def main() -> int:
|
|
3012
|
+
args = parse_args()
|
|
3013
|
+
try:
|
|
3014
|
+
selected_options = resolve_selected_options(args.include, args.exclude, preset=args.preset)
|
|
3015
|
+
except ValueError as exc:
|
|
3016
|
+
print(json.dumps({"error": str(exc)}, indent=2, ensure_ascii=False))
|
|
3017
|
+
return 2
|
|
3018
|
+
migrator = Migrator(
|
|
3019
|
+
source_root=Path(os.path.expanduser(args.source)).resolve(),
|
|
3020
|
+
target_root=Path(os.path.expanduser(args.target)).resolve(),
|
|
3021
|
+
execute=bool(args.execute),
|
|
3022
|
+
workspace_target=Path(os.path.expanduser(args.workspace_target)).resolve() if args.workspace_target else None,
|
|
3023
|
+
overwrite=bool(args.overwrite),
|
|
3024
|
+
migrate_secrets=bool(args.migrate_secrets),
|
|
3025
|
+
output_dir=Path(os.path.expanduser(args.output_dir)).resolve() if args.output_dir else None,
|
|
3026
|
+
selected_options=selected_options,
|
|
3027
|
+
preset_name=args.preset or "",
|
|
3028
|
+
skill_conflict_mode=args.skill_conflict,
|
|
3029
|
+
)
|
|
3030
|
+
report = migrator.migrate()
|
|
3031
|
+
|
|
3032
|
+
# ── 机器可读的 JSON 模式 ────────────────────────────
|
|
3033
|
+
# 当设置了 --json 时,将脱敏后的报告输出到标准输出并跳过
|
|
3034
|
+
# 人类可读的终端摘要。适用于 CI 和脚本化包装器。
|
|
3035
|
+
if getattr(args, "json_output", False):
|
|
3036
|
+
print(json.dumps(redact_migration_value(report), indent=2, ensure_ascii=False))
|
|
3037
|
+
return 0
|
|
3038
|
+
|
|
3039
|
+
# ── 人类可读的终端摘要 ─────────────────────────────────
|
|
3040
|
+
s = report["summary"]
|
|
3041
|
+
items = report["items"]
|
|
3042
|
+
mode_label = "DRY RUN" if not args.execute else "EXECUTED"
|
|
3043
|
+
total = sum(s.values())
|
|
3044
|
+
|
|
3045
|
+
print()
|
|
3046
|
+
print(f" ╔══════════════════════════════════════════════════════╗")
|
|
3047
|
+
print(f" ║ OpenClaw -> Hermes Migration [{mode_label:>8s}] ║")
|
|
3048
|
+
print(f" ╠══════════════════════════════════════════════════════╣")
|
|
3049
|
+
print(f" ║ Source: {str(report['source_root'])[:42]:<42s} ║")
|
|
3050
|
+
print(f" ║ Target: {str(report['target_root'])[:42]:<42s} ║")
|
|
3051
|
+
print(f" ╠══════════════════════════════════════════════════════╣")
|
|
3052
|
+
print(f" ║ ✔ Migrated: {s.get('migrated', 0):>3d} ◆ Archived: {s.get('archived', 0):>3d} ║")
|
|
3053
|
+
print(f" ║ ⊘ Skipped: {s.get('skipped', 0):>3d} ⚠ Conflicts: {s.get('conflict', 0):>3d} ║")
|
|
3054
|
+
print(f" ║ ✖ Errors: {s.get('error', 0):>3d} Total: {total:>3d} ║")
|
|
3055
|
+
print(f" ╚══════════════════════════════════════════════════════╝")
|
|
3056
|
+
|
|
3057
|
+
# 显示已迁移的内容
|
|
3058
|
+
migrated = [i for i in items if i["status"] == "migrated"]
|
|
3059
|
+
if migrated:
|
|
3060
|
+
print()
|
|
3061
|
+
print(" Migrated:")
|
|
3062
|
+
seen_kinds = set()
|
|
3063
|
+
for item in migrated:
|
|
3064
|
+
label = item["kind"]
|
|
3065
|
+
if label in seen_kinds:
|
|
3066
|
+
continue
|
|
3067
|
+
seen_kinds.add(label)
|
|
3068
|
+
dest = item.get("destination") or ""
|
|
3069
|
+
if dest.startswith(str(report["target_root"])):
|
|
3070
|
+
dest = "~/.hermes/" + dest[len(str(report["target_root"])) + 1:]
|
|
3071
|
+
meta = MIGRATION_OPTION_METADATA.get(label, {})
|
|
3072
|
+
display = meta.get("label", label)
|
|
3073
|
+
print(f" ✔ {display:<35s} -> {dest}")
|
|
3074
|
+
|
|
3075
|
+
# 显示已归档的内容
|
|
3076
|
+
archived = [i for i in items if i["status"] == "archived"]
|
|
3077
|
+
if archived:
|
|
3078
|
+
print()
|
|
3079
|
+
print(" Archived (manual review needed):")
|
|
3080
|
+
seen_kinds = set()
|
|
3081
|
+
for item in archived:
|
|
3082
|
+
label = item["kind"]
|
|
3083
|
+
if label in seen_kinds:
|
|
3084
|
+
continue
|
|
3085
|
+
seen_kinds.add(label)
|
|
3086
|
+
reason = item.get("reason", "")
|
|
3087
|
+
meta = MIGRATION_OPTION_METADATA.get(label, {})
|
|
3088
|
+
display = meta.get("label", label)
|
|
3089
|
+
short_reason = reason[:50] + "..." if len(reason) > 50 else reason
|
|
3090
|
+
print(f" ◆ {display:<35s} {short_reason}")
|
|
3091
|
+
|
|
3092
|
+
# 显示冲突
|
|
3093
|
+
conflicts = [i for i in items if i["status"] == "conflict"]
|
|
3094
|
+
if conflicts:
|
|
3095
|
+
print()
|
|
3096
|
+
print(" Conflicts (use --overwrite to force):")
|
|
3097
|
+
for item in conflicts:
|
|
3098
|
+
print(f" ⚠ {item['kind']}: {item.get('reason', '')}")
|
|
3099
|
+
|
|
3100
|
+
# 显示错误
|
|
3101
|
+
errors = [i for i in items if i["status"] == "error"]
|
|
3102
|
+
if errors:
|
|
3103
|
+
print()
|
|
3104
|
+
print(" Errors:")
|
|
3105
|
+
for item in errors:
|
|
3106
|
+
print(f" ✖ {item['kind']}: {item.get('reason', '')}")
|
|
3107
|
+
|
|
3108
|
+
# PM2 提示信息
|
|
3109
|
+
print()
|
|
3110
|
+
print(" ℹ PM2 processes (Discord/Telegram bots) are NOT affected.")
|
|
3111
|
+
|
|
3112
|
+
# 后续步骤
|
|
3113
|
+
if args.execute:
|
|
3114
|
+
print()
|
|
3115
|
+
print(" Next steps:")
|
|
3116
|
+
print(" 1. Review ~/.hermes/config.yaml")
|
|
3117
|
+
print(" 2. Run: hermes mcp list")
|
|
3118
|
+
if any(i["kind"] == "cron-jobs" and i["status"] == "archived" for i in items):
|
|
3119
|
+
print(" 3. Recreate cron jobs: hermes cron")
|
|
3120
|
+
if report.get("output_dir"):
|
|
3121
|
+
print(f" → Full report: {report['output_dir']}/MIGRATION_NOTES.md")
|
|
3122
|
+
elif not args.execute:
|
|
3123
|
+
print()
|
|
3124
|
+
print(" This was a dry run. Add --execute to apply changes.")
|
|
3125
|
+
|
|
3126
|
+
print()
|
|
3127
|
+
|
|
3128
|
+
# 同时导出 JSON 以供程序化使用
|
|
3129
|
+
if os.environ.get("MIGRATION_JSON_OUTPUT"):
|
|
3130
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
3131
|
+
|
|
3132
|
+
return 0 if s.get("error", 0) == 0 else 1
|
|
3133
|
+
|
|
3134
|
+
|
|
3135
|
+
if __name__ == "__main__":
|
|
3136
|
+
raise SystemExit(main())
|