diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4ef00213562..5908b703fea 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1742,7 +1742,17 @@ jobs:
with:
install-bun: "false"
+ - name: Checkout ClawHub docs source
+ uses: actions/checkout@v6
+ with:
+ repository: openclaw/clawhub
+ path: clawhub-source
+ fetch-depth: 1
+ persist-credentials: false
+
- name: Check docs
+ env:
+ OPENCLAW_DOCS_SYNC_CLAWHUB_REPO: ${{ github.workspace }}/clawhub-source
run: pnpm check:docs
skills-python:
diff --git a/.github/workflows/docs-sync-publish.yml b/.github/workflows/docs-sync-publish.yml
index 0e367bc6003..fd00e7d016e 100644
--- a/.github/workflows/docs-sync-publish.yml
+++ b/.github/workflows/docs-sync-publish.yml
@@ -22,6 +22,15 @@ jobs:
with:
fetch-depth: 0
+ - name: Checkout ClawHub docs source
+ uses: actions/checkout@v6
+ with:
+ repository: openclaw/clawhub
+ path: clawhub-source
+ fetch-depth: 1
+ persist-credentials: false
+ token: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN || github.token }}
+
- name: Setup Node
uses: actions/setup-node@v6
with:
@@ -48,12 +57,17 @@ jobs:
- name: Sync docs into publish repo
run: |
+ clawhub_sha="$(git -C "$GITHUB_WORKSPACE/clawhub-source" rev-parse HEAD)"
node scripts/docs-sync-publish.mjs \
--target "$GITHUB_WORKSPACE/publish" \
--source-repo "$GITHUB_REPOSITORY" \
- --source-sha "$GITHUB_SHA"
+ --source-sha "$GITHUB_SHA" \
+ --clawhub-repo "$GITHUB_WORKSPACE/clawhub-source" \
+ --clawhub-source-repo "openclaw/clawhub" \
+ --clawhub-source-sha "$clawhub_sha"
- name: Install docs MDX checker dependency
+ working-directory: publish
run: npm install --no-save --package-lock=false @mdx-js/mdx@3.1.1
- name: Check publish docs MDX
diff --git a/docs/.i18n/zh-Hans-navigation.json b/docs/.i18n/zh-Hans-navigation.json
index 1f2a7b1aebf..4380ee2ca23 100644
--- a/docs/.i18n/zh-Hans-navigation.json
+++ b/docs/.i18n/zh-Hans-navigation.json
@@ -203,7 +203,6 @@
"zh-CN/tools/slash-commands",
"zh-CN/tools/skills",
"zh-CN/tools/skills-config",
- "zh-CN/tools/clawhub",
"zh-CN/tools/plugin"
]
},
diff --git a/docs/automation/taskflow.md b/docs/automation/taskflow.md
index db9c6390e4d..76e75376adf 100644
--- a/docs/automation/taskflow.md
+++ b/docs/automation/taskflow.md
@@ -90,7 +90,7 @@ Recommended data provenance fields for every collected item:
Have the workflow reject or mark stale items before summarization. The LLM step should receive only structured JSON and should be asked to preserve `sourceUrl`, `retrievedAt`, and `asOf` in its output. Use [LLM Task](/tools/llm-task) when you need a schema-validated model step inside the workflow.
-For reusable team or community workflows, package the CLI, `.lobster` files, and any setup notes as a skill or plugin and publish it through [ClawHub](/tools/clawhub). Keep workflow-specific guardrails in that package unless the plugin API is missing a needed generic capability.
+For reusable team or community workflows, package the CLI, `.lobster` files, and any setup notes as a skill or plugin and publish it through [ClawHub](/clawhub). Keep workflow-specific guardrails in that package unless the plugin API is missing a needed generic capability.
## Sync modes
diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md
index 217eaa34e22..dd7c813df4d 100644
--- a/docs/cli/plugins.md
+++ b/docs/cli/plugins.md
@@ -129,7 +129,7 @@ is available, then fall back to `latest`.
This CLI flag applies to plugin install/update flows. Gateway-backed skill dependency installs use the matching `dangerouslyForceUnsafeInstall` request override, while `openclaw skills install` remains a separate ClawHub skill download/install flow.
- If a plugin you published on ClawHub is blocked by a registry scan, use the publisher steps in [ClawHub](/tools/clawhub).
+ If a plugin you published on ClawHub is blocked by a registry scan, use the publisher steps in [ClawHub](/clawhub/security).
@@ -417,4 +417,4 @@ Marketplace list accepts a local marketplace path, a `marketplace.json` path, a
- [Building plugins](/plugins/building-plugins)
- [CLI reference](/cli)
-- [Community plugins](/plugins/community)
+- [ClawHub](/clawhub)
diff --git a/docs/cli/skills.md b/docs/cli/skills.md
index a3949134769..e2775899ff9 100644
--- a/docs/cli/skills.md
+++ b/docs/cli/skills.md
@@ -15,7 +15,7 @@ Related:
- Skills system: [Skills](/tools/skills)
- Skills config: [Skills config](/tools/skills-config)
-- ClawHub installs: [ClawHub](/tools/clawhub)
+- ClawHub installs: [ClawHub](/clawhub/cli)
## Commands
diff --git a/docs/docs.json b/docs/docs.json
index 0372237ab49..75e7df30000 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -393,16 +393,16 @@
"destination": "/channels/pairing"
},
{
- "source": "/clawhub",
- "destination": "/tools/clawhub"
+ "source": "/clawdhub",
+ "destination": "/clawhub"
},
{
- "source": "/clawdhub",
- "destination": "/tools/clawhub"
+ "source": "/tools/clawhub",
+ "destination": "/clawhub"
},
{
"source": "/tools/clawdhub",
- "destination": "/tools/clawhub"
+ "destination": "/clawhub"
},
{
"source": "/configuration",
@@ -1242,7 +1242,6 @@
"tools/creating-skills",
"tools/skills-config",
"tools/slash-commands",
- "tools/clawhub",
"prose"
]
},
@@ -1325,6 +1324,36 @@
}
]
},
+ {
+ "tab": "ClawHub",
+ "groups": [
+ {
+ "group": "Overview",
+ "pages": ["clawhub/index", "clawhub/quickstart", "clawhub/how-it-works"]
+ },
+ {
+ "group": "Using ClawHub",
+ "pages": [
+ "clawhub/cli",
+ "clawhub/publishing",
+ "clawhub/skill-format",
+ "clawhub/soul-format",
+ "clawhub/auth",
+ "clawhub/telemetry",
+ "clawhub/troubleshooting"
+ ]
+ },
+ {
+ "group": "API and trust",
+ "pages": [
+ "clawhub/api",
+ "clawhub/http-api",
+ "clawhub/security",
+ "clawhub/acceptable-usage"
+ ]
+ }
+ ]
+ },
{
"tab": "Models",
"groups": [
diff --git a/docs/help/faq.md b/docs/help/faq.md
index 63f56c25f1a..6467f2408fe 100644
--- a/docs/help/faq.md
+++ b/docs/help/faq.md
@@ -409,7 +409,7 @@ lives on the [First-run FAQ](/help/faq-first-run).
openclaw skills update --all
```
- Native installs land in the active workspace `skills/` directory. For shared skills across agents, place them in `~/.openclaw/skills//SKILL.md`. If only some agents should see a shared install, configure `agents.defaults.skills` or `agents.list[].skills`. Some skills expect binaries installed via Homebrew; on Linux that means Linuxbrew (see the Homebrew Linux FAQ entry above). See [Skills](/tools/skills), [Skills config](/tools/skills-config), and [ClawHub](/tools/clawhub).
+ Native installs land in the active workspace `skills/` directory. For shared skills across agents, place them in `~/.openclaw/skills//SKILL.md`. If only some agents should see a shared install, configure `agents.defaults.skills` or `agents.list[].skills`. Some skills expect binaries installed via Homebrew; on Linux that means Linuxbrew (see the Homebrew Linux FAQ entry above). See [Skills](/tools/skills), [Skills config](/tools/skills-config), and [ClawHub](/clawhub).
diff --git a/docs/plugins/building-plugins.md b/docs/plugins/building-plugins.md
index efd88963ffc..27e34a4d216 100644
--- a/docs/plugins/building-plugins.md
+++ b/docs/plugins/building-plugins.md
@@ -14,7 +14,7 @@ generation, video generation, web fetch, web search, agent tools, or any
combination.
You do not need to add your plugin to the OpenClaw repository. Publish to
-[ClawHub](/tools/clawhub) and users install with
+[ClawHub](/clawhub) and users install with
`openclaw plugins install clawhub:`. Bare package specs still
install from npm during the launch cutover.
diff --git a/docs/plugins/community.md b/docs/plugins/community.md
index 1e777e87917..aebc7b44a84 100644
--- a/docs/plugins/community.md
+++ b/docs/plugins/community.md
@@ -8,7 +8,7 @@ title: "Community plugins"
Community plugins are third-party packages that extend OpenClaw with new
channels, tools, providers, or other capabilities. They are built and maintained
-by the community, usually published on [ClawHub](/tools/clawhub), and installable
+by the community, usually published on [ClawHub](/clawhub), and installable
with a single command. Npm remains the launch default for bare package specs
while ClawHub pack installs roll out.
@@ -152,7 +152,7 @@ We welcome community plugins that are useful, documented, and safe to operate.
Your plugin must be installable via `openclaw plugins install \`.
- Publish to [ClawHub](/tools/clawhub) unless you specifically need npm-only
+ Publish to [ClawHub](/clawhub) unless you specifically need npm-only
distribution.
See [Building Plugins](/plugins/building-plugins) for the full guide.
diff --git a/docs/plugins/manage-plugins.md b/docs/plugins/manage-plugins.md
index 5d882a76ef2..da26ad61cda 100644
--- a/docs/plugins/manage-plugins.md
+++ b/docs/plugins/manage-plugins.md
@@ -187,6 +187,6 @@ forces npm resolution.
- [Plugins](/tools/plugin) - overview and troubleshooting
- [`openclaw plugins`](/cli/plugins) - full CLI reference
-- [ClawHub](/tools/clawhub) - publish and registry operations
+- [ClawHub](/clawhub/cli) - publish and registry operations
- [Building plugins](/plugins/building-plugins) - create a plugin package
- [Plugin manifest](/plugins/manifest) - manifest and package metadata
diff --git a/docs/plugins/sdk-setup.md b/docs/plugins/sdk-setup.md
index 1324c693441..2ccc6b8cd7c 100644
--- a/docs/plugins/sdk-setup.md
+++ b/docs/plugins/sdk-setup.md
@@ -492,7 +492,7 @@ The `ChannelSetupWizard` type supports `credentials`, `textInputs`, `dmPolicy`,
## Publishing and installing
-**External plugins:** publish to [ClawHub](/tools/clawhub), then install:
+**External plugins:** publish to [ClawHub](/clawhub), then install:
diff --git a/docs/plugins/zalouser.md b/docs/plugins/zalouser.md
index 91e75d8029e..9885aab35fd 100644
--- a/docs/plugins/zalouser.md
+++ b/docs/plugins/zalouser.md
@@ -83,4 +83,4 @@ Channel message actions also support `react` for message reactions.
## Related
- [Building plugins](/plugins/building-plugins)
-- [Community plugins](/plugins/community)
+- [ClawHub](/clawhub)
diff --git a/docs/start/hubs.md b/docs/start/hubs.md
index 7d00a56fd69..45b939f78cb 100644
--- a/docs/start/hubs.md
+++ b/docs/start/hubs.md
@@ -167,7 +167,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
- [Plugin manifest](/plugins/manifest)
- [Agent tools](/plugins/building-plugins#registering-agent-tools)
- [Plugin bundles](/plugins/bundles)
-- [Community plugins](/plugins/community)
+- [ClawHub](/clawhub)
- [Capability cookbook](/tools/capability-cookbook)
- [Voice call plugin](/plugins/voice-call)
- [Zalo user plugin](/plugins/zalouser)
@@ -175,7 +175,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
## Workspace + templates
- [Skills](/tools/skills)
-- [ClawHub](/tools/clawhub)
+- [ClawHub](/clawhub)
- [Skills config](/tools/skills-config)
- [Default AGENTS](/reference/AGENTS.default)
- [Templates: AGENTS](/reference/templates/AGENTS)
diff --git a/docs/tools/clawhub.md b/docs/tools/clawhub.md
index 987efaefe26..f63db895d71 100644
--- a/docs/tools/clawhub.md
+++ b/docs/tools/clawhub.md
@@ -1,470 +1,5 @@
---
-summary: "ClawHub: public registry for OpenClaw skills and plugins, native install flows, and the clawhub CLI"
-read_when:
- - Searching for, installing, or updating skills or plugins
- - Publishing skills or plugins to the registry
- - Configuring the clawhub CLI or its environment overrides
-title: "ClawHub"
-sidebarTitle: "ClawHub"
+summary: "Redirect to /clawhub"
+title: "ClawHub (redirect)"
+redirect: /clawhub
---
-
-ClawHub is the public registry for **OpenClaw skills and plugins**.
-
-- Use native `openclaw` commands to search, install, and update skills, and to install plugins from ClawHub.
-- Use the separate `clawhub` CLI for registry auth, publish, delete/undelete, and sync workflows.
-
-Site: [clawhub.ai](https://clawhub.ai)
-
-## Quick start
-
-
-
- ```bash
- openclaw skills search "calendar"
- ```
-
-
- ```bash
- openclaw skills install
- ```
-
-
- Start a new OpenClaw session - it picks up the new skill.
-
-
- For registry-authenticated workflows (publish, sync, manage), install
- the separate `clawhub` CLI:
-
- ```bash
- npm i -g clawhub
- # or
- pnpm add -g clawhub
- ```
-
-
-
-
-## Native OpenClaw flows
-
-
-
- ```bash
- openclaw skills search "calendar"
- openclaw skills install
- openclaw skills update --all
- ```
-
- Native `openclaw` commands install into your active workspace and
- persist source metadata so later `update` calls can stay on ClawHub.
-
-
-
- ```bash
- openclaw plugins search "calendar"
- openclaw plugins install clawhub:
- openclaw plugins update --all
- ```
-
- `plugins search` queries the ClawHub plugin catalog and prints install-ready
- package names. Use `clawhub:` when you want ClawHub resolution.
- Bare npm-safe plugin specs install from npm during the launch cutover:
-
- ```bash
- openclaw plugins install openclaw-codex-app-server
- ```
-
- `npm:` is also npm-only and is useful when a spec could otherwise
- be ambiguous:
-
- ```bash
- openclaw plugins install npm:openclaw-codex-app-server
- ```
-
- Plugin installs validate advertised `pluginApi` and
- `minGatewayVersion` compatibility before archive install runs, so
- incompatible hosts fail closed early instead of partially installing
- the package. When a package version publishes a ClawPack artifact,
- OpenClaw prefers the exact uploaded npm-pack `.tgz`, verifies the ClawHub
- digest header and downloaded bytes, and records the artifact kind, npm
- integrity, npm shasum, tarball name, and ClawPack digest metadata for later
- updates. Older package versions without ClawPack metadata still use the
- legacy package archive verification path.
-
-
-
-
-
-`openclaw plugins install clawhub:...` only accepts installable plugin
-families. If a ClawHub package is actually a skill, OpenClaw stops and
-points you at `openclaw skills install ` instead.
-
-Anonymous ClawHub plugin installs also fail closed for private packages.
-Community or other non-official channels can still install, but OpenClaw
-warns so operators can review source and verification before enabling
-them.
-
-
-## What ClawHub is
-
-- A public registry for OpenClaw skills and plugins.
-- A versioned store of skill bundles and metadata.
-- A discovery surface for search, tags, and usage signals.
-
-A typical skill is a versioned bundle of files that includes:
-
-- A `SKILL.md` file with the primary description and usage.
-- Optional configs, scripts, or supporting files used by the skill.
-- Metadata such as tags, summary, and install requirements.
-
-ClawHub uses metadata to power discovery and safely expose skill
-capabilities. The registry tracks usage signals (stars, downloads) to
-improve ranking and visibility. Each publish creates a new semver
-version, and the registry keeps version history so users can audit
-changes.
-
-## Workspace and skill loading
-
-The separate `clawhub` CLI also installs skills into `./skills` under
-your current working directory. If an OpenClaw workspace is configured,
-`clawhub` falls back to that workspace unless you override `--workdir`
-(or `CLAWHUB_WORKDIR`). OpenClaw loads workspace skills from
-`/skills` and picks them up in the **next** session.
-
-If you already use `~/.openclaw/skills` or bundled skills, workspace
-skills take precedence. For more detail on how skills are loaded,
-shared, and gated, see [Skills](/tools/skills).
-
-## Service features
-
-| Feature | Notes |
-| ------------------------ | ------------------------------------------------------------------- |
-| Public browsing | Skills and their `SKILL.md` content are publicly viewable. |
-| Search | Embedding-powered (vector search), not just keywords. |
-| Versioning | Semver, changelogs, and tags (including `latest`). |
-| Downloads | Zip per version. |
-| Stars and comments | Community feedback. |
-| Security scan summaries | Detail pages show the latest scan state before install or download. |
-| Scanner detail pages | VirusTotal, ClawScan, and static-analysis results have deep links. |
-| Owner recovery dashboard | Publishers can see scan-held owned content from `/dashboard`. |
-| Owner-requested rescans | Owners can request limited rescans for false-positive recovery. |
-| Moderation | Approvals and audits. |
-| CLI-friendly API | Suitable for automation and scripting. |
-
-## Security and moderation
-
-ClawHub is open by default - anyone can upload skills, but a GitHub
-account must be **at least one week old** to publish. This slows down
-abuse without blocking legitimate contributors.
-
-
-
- ClawHub runs automated security checks on published skills and plugin
- releases. Public detail pages summarize the current result, and scanner
- rows link to dedicated detail pages for VirusTotal, ClawScan, and static
- analysis.
-
- Scan-held or blocked releases may be unavailable on public catalog and
- install surfaces while still visible to their owner in `/dashboard`.
-
-
-
- - Any signed-in user can report a skill.
- - Report reasons are required and recorded.
- - Each user can have up to 20 active reports at a time.
- - Skills with more than 3 unique reports are auto-hidden by default.
-
-
-
- - Moderators can view hidden skills, unhide them, delete them, or ban users.
- - Abusing the report feature can result in account bans.
- - Interested in becoming a moderator? Ask in the OpenClaw Discord and contact a moderator or maintainer.
-
-
-
-
-## ClawHub CLI
-
-You only need this for registry-authenticated workflows such as
-publish/sync.
-
-### Global options
-
-
- Working directory. Default: current dir; falls back to OpenClaw workspace.
-
-
- Skills directory, relative to workdir.
-
-
- Site base URL (browser login).
-
-
- Registry API base URL.
-
-
- Disable prompts (non-interactive).
-
-
- Print CLI version.
-
-
-### Commands
-
-
-
- ```bash
- clawhub login # browser flow
- clawhub login --token
- clawhub logout
- clawhub whoami
- ```
-
- Login options:
-
- - `--token ` - paste an API token.
- - `--label
-
- ```bash
- clawhub search "query"
- ```
-
- Searches skills. For plugin/package discovery, use `clawhub package explore`.
-
- - `--limit ` - max results.
-
-
-
- ```bash
- clawhub package explore --family code-plugin
- clawhub package explore "episodic-claw" --family code-plugin
- clawhub package inspect episodic-claw
- ```
-
- `package explore` and `package inspect` are the ClawHub CLI surfaces for plugin/package discovery and metadata inspection. Native OpenClaw installs still use `openclaw plugins install clawhub:`.
-
- Options:
-
- - `--family skill|code-plugin|bundle-plugin` - filter package family.
- - `--official` - show only official packages.
- - `--executes-code` - show only packages that execute code.
- - `--version ` / `--tag ` - inspect a specific package version.
- - `--versions`, `--files`, `--file ` - inspect package history and files.
- - `--json` - machine-readable output.
-
-
-
- ```bash
- clawhub install
- clawhub update
- clawhub update --all
- clawhub list
- ```
-
- Options:
-
- - `--version ` - install or update to a specific version (single slug only on `update`).
- - `--force` - overwrite if the folder already exists, or when local files do not match any published version.
- - `clawhub list` reads `.clawhub/lock.json`.
-
-
-
- ```bash
- clawhub skill publish
- ```
-
- Options:
-
- - `--slug ` - skill slug.
- - `--name ` - display name.
- - `--version ` - semver version.
- - `--changelog ` - changelog text (can be empty).
- - `--tags ` - comma-separated tags (default: `latest`).
-
-
-
- ```bash
- clawhub package publish
- ```
-
- `` can be a local folder, `owner/repo`, `owner/repo@ref`, or a
- GitHub URL.
-
- Options:
-
- - `--dry-run` - build the exact publish plan without uploading anything.
- - `--json` - emit machine-readable output for CI.
- - `--source-repo`, `--source-commit`, `--source-ref` - optional overrides when auto-detection is not enough.
-
-
-
- ```bash
- clawhub skill rescan
- clawhub skill rescan --yes --json
-
- clawhub package rescan
- clawhub package rescan --yes --json
- ```
-
- Rescan commands require a logged-in owner token and target the latest
- published skill version or plugin release. In non-interactive runs, pass
- `--yes`.
-
- JSON responses include the target kind, name, version, rescan status, and
- remaining/max request counts for that version or release.
-
-
-
- ```bash
- clawhub delete --yes
- clawhub undelete --yes
- ```
-
-
- ```bash
- clawhub sync
- ```
-
- Options:
-
- - `--root ` - extra scan roots.
- - `--all` - upload everything without prompts.
- - `--dry-run` - show what would be uploaded.
- - `--bump ` - `patch|minor|major` for updates (default: `patch`).
- - `--changelog ` - changelog for non-interactive updates.
- - `--tags ` - comma-separated tags (default: `latest`).
- - `--concurrency ` - registry checks (default: `4`).
-
-
-
-
-## Common workflows
-
-
-
- ```bash
- clawhub search "postgres backups"
- ```
-
-
- ```bash
- clawhub package explore --family code-plugin
- clawhub package explore "memory" --family code-plugin
- clawhub package inspect episodic-claw
- ```
-
-
- ```bash
- clawhub install my-skill-pack
- ```
-
-
- ```bash
- clawhub update --all
- ```
-
-
- ```bash
- clawhub skill publish ./my-skill --slug my-skill --name "My Skill" --version 1.0.0 --tags latest
- ```
-
-
- ```bash
- clawhub sync --all
- ```
-
-
- ```bash
- clawhub package publish your-org/your-plugin --dry-run
- clawhub package publish your-org/your-plugin
- clawhub package publish your-org/your-plugin@v1.0.0
- clawhub package publish https://github.com/your-org/your-plugin
- ```
-
-
-
-### Plugin package metadata
-
-Code plugins must include the required OpenClaw metadata in
-`package.json`:
-
-```json
-{
- "name": "@myorg/openclaw-my-plugin",
- "version": "1.0.0",
- "type": "module",
- "openclaw": {
- "extensions": ["./src/index.ts"],
- "runtimeExtensions": ["./dist/index.js"],
- "compat": {
- "pluginApi": ">=2026.3.24-beta.2",
- "minGatewayVersion": "2026.3.24-beta.2"
- },
- "build": {
- "openclawVersion": "2026.3.24-beta.2",
- "pluginSdkVersion": "2026.3.24-beta.2"
- }
- }
-}
-```
-
-Published packages should ship **built JavaScript** and point
-`runtimeExtensions` at that output. Git checkout installs can still fall
-back to TypeScript source when no built files exist, but built runtime
-entries avoid runtime TypeScript compilation in startup, doctor, and
-plugin loading paths.
-
-## Versioning, lockfile, and telemetry
-
-
-
- - Each publish creates a new **semver** `SkillVersion`.
- - Tags (like `latest`) point to a version; moving tags lets you roll back.
- - Changelogs are attached per version and can be empty when syncing or publishing updates.
-
-
-
- Updates compare the local skill contents to registry versions using a
- content hash. If local files do not match any published version, the
- CLI asks before overwriting (or requires `--force` in
- non-interactive runs).
-
-
- `clawhub sync` scans your current workdir first. If no skills are
- found, it falls back to known legacy locations (for example
- `~/openclaw/skills` and `~/.openclaw/skills`). This is designed to
- find older skill installs without extra flags.
-
-
- - Installed skills are recorded in `.clawhub/lock.json` under your workdir.
- - Auth tokens are stored in the ClawHub CLI config file (override via `CLAWHUB_CONFIG_PATH`).
-
-
-
- When you run `clawhub sync` while logged in, the CLI sends a minimal
- snapshot to compute install counts. You can disable this entirely:
-
- ```bash
- export CLAWHUB_DISABLE_TELEMETRY=1
- ```
-
-
-
-
-## Environment variables
-
-| Variable | Effect |
-| ----------------------------- | ----------------------------------------------- |
-| `CLAWHUB_SITE` | Override the site URL. |
-| `CLAWHUB_REGISTRY` | Override the registry API URL. |
-| `CLAWHUB_CONFIG_PATH` | Override where the CLI stores the token/config. |
-| `CLAWHUB_WORKDIR` | Override the default workdir. |
-| `CLAWHUB_DISABLE_TELEMETRY=1` | Disable telemetry on `sync`. |
-
-## Related
-
-- [Community plugins](/plugins/community)
-- [Plugins](/tools/plugin)
-- [Skills](/tools/skills)
diff --git a/docs/tools/creating-skills.md b/docs/tools/creating-skills.md
index 624c0e223f8..d23b1caff02 100644
--- a/docs/tools/creating-skills.md
+++ b/docs/tools/creating-skills.md
@@ -116,5 +116,5 @@ The YAML frontmatter supports these fields:
- [Skills reference](/tools/skills) — loading, precedence, and gating rules
- [Skills config](/tools/skills-config) — `skills.*` config schema
-- [ClawHub](/tools/clawhub) — public skill registry
+- [ClawHub](/clawhub) — public skill registry
- [Building Plugins](/plugins/building-plugins) — plugins can ship skills
diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md
index e5c70c1e8b9..2dfe5f3d116 100644
--- a/docs/tools/plugin.md
+++ b/docs/tools/plugin.md
@@ -13,7 +13,7 @@ agent harnesses, tools, skills, speech, realtime transcription, realtime
voice, media-understanding, image generation, video generation, web fetch, web
search, and more. Some plugins are **core** (shipped with OpenClaw), others
are **external**. Most external plugins are published and discovered through
-[ClawHub](/tools/clawhub). Npm remains supported for direct installs and for a
+[ClawHub](/clawhub). Npm remains supported for direct installs and for a
temporary set of OpenClaw-owned plugin packages while that migration finishes.
## Quick start
@@ -279,7 +279,7 @@ current OpenClaw or a local checkout until a newer npm package is published.
-Looking for third-party plugins? See [Community Plugins](/plugins/community).
+Looking for third-party plugins? See [ClawHub](/clawhub).
## Configuration
@@ -728,4 +728,4 @@ For full typed hook behavior, see [SDK overview](/plugins/sdk-overview#hook-deci
- [Plugin manifest](/plugins/manifest) - manifest schema
- [Registering tools](/plugins/building-plugins#registering-agent-tools) - add agent tools in a plugin
- [Plugin internals](/plugins/architecture) - capability model and load pipeline
-- [Community plugins](/plugins/community) - third-party listings
+- [ClawHub](/clawhub) - third-party plugin discovery
diff --git a/docs/tools/skills.md b/docs/tools/skills.md
index 626cde60df0..6e5fe577dc3 100644
--- a/docs/tools/skills.md
+++ b/docs/tools/skills.md
@@ -125,7 +125,7 @@ its proposals. Full guide: [Skill Workshop plugin](/plugins/skill-workshop).
[ClawHub](https://clawhub.ai) is the public skills registry for OpenClaw.
Use native `openclaw skills` commands for discover/install/update, or the
separate `clawhub` CLI for publish/sync workflows. Full guide:
-[ClawHub](/tools/clawhub).
+[ClawHub](/clawhub).
| Action | Command |
| ---------------------------------- | -------------------------------------- |
@@ -475,7 +475,7 @@ schema: [Skills config](/tools/skills-config).
## Related
-- [ClawHub](/tools/clawhub) - public skills registry
+- [ClawHub](/clawhub) - public skills registry
- [Creating skills](/tools/creating-skills) - building custom skills
- [Plugins](/tools/plugin) - plugin system overview
- [Skill Workshop plugin](/plugins/skill-workshop) - generate skills from agent work
diff --git a/scripts/docs-link-audit.mjs b/scripts/docs-link-audit.mjs
index 85da5c4ac3a..1ff01e4094d 100644
--- a/scripts/docs-link-audit.mjs
+++ b/scripts/docs-link-audit.mjs
@@ -5,6 +5,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
+import { resolveClawHubRepoPath, syncClawHubDocsTree } from "./docs-sync-publish.mjs";
const ROOT = process.cwd();
const DOCS_DIR = path.join(ROOT, "docs");
@@ -59,17 +60,6 @@ function stripInlineCode(text) {
return text.replace(/`[^`]+`/g, "");
}
-const docsConfig = JSON.parse(fs.readFileSync(DOCS_JSON_PATH, "utf8"));
-const redirects = new Map();
-for (const item of docsConfig.redirects || []) {
- const source = normalizeRoute(item.source || "");
- const destination = normalizeRoute(item.destination || "");
- redirects.set(source, destination);
-}
-
-const allFiles = walk(DOCS_DIR);
-const relAllFiles = new Set(allFiles.map((abs) => normalizeSlashes(path.relative(DOCS_DIR, abs))));
-
function isLocalizedDocPath(p) {
return /^\/?[a-z]{2}(?:-[A-Za-z]{2,8})+\//.test(p);
}
@@ -78,40 +68,82 @@ function isGeneratedTranslatedDoc(relPath) {
return isLocalizedDocPath(relPath);
}
-const markdownFiles = allFiles.filter((abs) => {
- if (!/\.(md|mdx)$/i.test(abs)) {
- return false;
- }
- const rel = normalizeSlashes(path.relative(DOCS_DIR, abs));
- return !isGeneratedTranslatedDoc(rel);
-});
-const routes = new Set();
-
-for (const abs of markdownFiles) {
- const rel = normalizeSlashes(path.relative(DOCS_DIR, abs));
- const text = fs.readFileSync(abs, "utf8");
- const slug = rel.replace(/\.(md|mdx)$/i, "");
+function addRoute(routes, slug) {
const route = normalizeRoute(slug);
routes.add(route);
if (slug.endsWith("/index")) {
routes.add(normalizeRoute(slug.slice(0, -"/index".length)));
}
+}
- if (!text.startsWith("---")) {
- continue;
+function createRedirectMap(docsConfig) {
+ const redirects = new Map();
+ for (const item of docsConfig.redirects || []) {
+ const source = normalizeRoute(item.source || "");
+ const destination = normalizeRoute(item.destination || "");
+ redirects.set(source, destination);
+ }
+ return redirects;
+}
+
+function buildAuditIndex(docsDir = DOCS_DIR, options = {}) {
+ const docsJsonPath = path.join(docsDir, "docs.json");
+ const docsConfig = JSON.parse(fs.readFileSync(docsJsonPath, "utf8"));
+ const redirects = createRedirectMap(docsConfig);
+ const allFiles = walk(docsDir);
+ const relAllFiles = new Set(allFiles.map((abs) => normalizeSlashes(path.relative(docsDir, abs))));
+ const markdownFiles = allFiles.filter((abs) => {
+ if (!/\.(md|mdx)$/i.test(abs)) {
+ return false;
+ }
+ const rel = normalizeSlashes(path.relative(docsDir, abs));
+ return !isGeneratedTranslatedDoc(rel);
+ });
+ const routes = new Set();
+
+ for (const abs of markdownFiles) {
+ const rel = normalizeSlashes(path.relative(docsDir, abs));
+ const text = fs.readFileSync(abs, "utf8");
+ const slug = rel.replace(/\.(md|mdx)$/i, "");
+ addRoute(routes, slug);
+
+ if (!text.startsWith("---")) {
+ continue;
+ }
+
+ const end = text.indexOf("\n---", 3);
+ if (end === -1) {
+ continue;
+ }
+ const frontMatter = text.slice(3, end);
+ const match = frontMatter.match(/^permalink:\s*(.+)\s*$/m);
+ if (!match) {
+ continue;
+ }
+ const permalink = match[1].trim().replace(/^['"]|['"]$/g, "");
+ routes.add(normalizeRoute(permalink));
}
- const end = text.indexOf("\n---", 3);
- if (end === -1) {
- continue;
+ if (options.allowExternalClawHubRoutes === true) {
+ for (const page of collectNavPageEntries(docsConfig.navigation || [])) {
+ if (isGeneratedTranslatedDoc(page)) {
+ continue;
+ }
+ const route = normalizeRoute(page);
+ if (route === "/clawhub" || route.startsWith("/clawhub/")) {
+ addRoute(routes, page);
+ }
+ }
}
- const frontMatter = text.slice(3, end);
- const match = frontMatter.match(/^permalink:\s*(.+)\s*$/m);
- if (!match) {
- continue;
- }
- const permalink = match[1].trim().replace(/^['"]|['"]$/g, "");
- routes.add(normalizeRoute(permalink));
+
+ return { docsDir, docsConfig, redirects, allFiles, relAllFiles, markdownFiles, routes };
+}
+
+let defaultAuditIndex;
+
+function getDefaultAuditIndex() {
+ defaultAuditIndex ??= buildAuditIndex(DOCS_DIR);
+ return defaultAuditIndex;
}
/**
@@ -119,8 +151,9 @@ for (const abs of markdownFiles) {
* @param {{redirects?: Map, routes?: Set}} [options]
*/
export function resolveRoute(route, options = {}) {
- const redirectMap = options.redirects ?? redirects;
- const publishedRoutes = options.routes ?? routes;
+ const defaultIndex = options.redirects && options.routes ? undefined : getDefaultAuditIndex();
+ const redirectMap = options.redirects ?? defaultIndex.redirects;
+ const publishedRoutes = options.routes ?? defaultIndex.routes;
let current = normalizeRoute(route);
if (current === "/") {
return { ok: true, terminal: "/" };
@@ -241,6 +274,27 @@ export function sanitizeDocsConfigForEnglishOnly(value) {
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
}
+function prepareMirroredDocsDir(sourceDir = DOCS_DIR) {
+ const sourceRoot = path.resolve(sourceDir);
+ if (sourceRoot !== path.resolve(DOCS_DIR)) {
+ return { dir: sourceRoot, mirroredClawHub: false, cleanup: () => {} };
+ }
+
+ const clawhubRepo = resolveClawHubRepoPath("", { required: false });
+ if (!clawhubRepo) {
+ return { dir: sourceRoot, mirroredClawHub: false, cleanup: () => {} };
+ }
+
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docs-link-audit-"));
+ fs.cpSync(sourceRoot, tempDir, { recursive: true });
+ syncClawHubDocsTree(tempDir, { repoPath: clawhubRepo, required: false });
+ return {
+ dir: tempDir,
+ mirroredClawHub: true,
+ cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
+ };
+}
+
export function prepareAnchorAuditDocsDir(sourceDir = DOCS_DIR) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docs-anchor-audit-"));
fs.cpSync(sourceDir, tempDir, { recursive: true });
@@ -313,13 +367,17 @@ export function resolveMintlifyAnchorAuditInvocation(params) {
return { command: "pnpm", args: MINTLIFY_BROKEN_LINKS_ARGS };
}
-export function auditDocsLinks() {
+export function auditDocsLinks(options = {}) {
+ const docsDir = options.docsDir ?? DOCS_DIR;
+ const index = buildAuditIndex(docsDir, {
+ allowExternalClawHubRoutes: options.allowExternalClawHubRoutes === true,
+ });
/** @type {{file: string; line: number; link: string; reason: string}[]} */
const broken = [];
let checked = 0;
- for (const abs of markdownFiles) {
- const rel = normalizeSlashes(path.relative(DOCS_DIR, abs));
+ for (const abs of index.markdownFiles) {
+ const rel = normalizeSlashes(path.relative(index.docsDir, abs));
const baseDir = normalizeSlashes(path.dirname(rel));
const rawText = fs.readFileSync(abs, "utf8");
const lines = rawText.split("\n");
@@ -357,10 +415,13 @@ export function auditDocsLinks() {
if (clean.startsWith("/")) {
const route = normalizeRoute(clean);
- const resolvedRoute = resolveRoute(route);
+ const resolvedRoute = resolveRoute(route, {
+ redirects: index.redirects,
+ routes: index.routes,
+ });
if (!resolvedRoute.ok) {
const staticRel = route.replace(/^\//, "");
- if (!relAllFiles.has(staticRel)) {
+ if (!index.relAllFiles.has(staticRel)) {
broken.push({
file: rel,
line: lineNum + 1,
@@ -380,7 +441,7 @@ export function auditDocsLinks() {
const normalizedRel = normalizeSlashes(path.normalize(path.join(baseDir, clean)));
if (/\.[a-zA-Z0-9]+$/.test(normalizedRel)) {
- if (!relAllFiles.has(normalizedRel)) {
+ if (!index.relAllFiles.has(normalizedRel)) {
broken.push({
file: rel,
line: lineNum + 1,
@@ -399,7 +460,7 @@ export function auditDocsLinks() {
`${normalizedRel}/index.mdx`,
];
- if (!candidates.some((candidate) => relAllFiles.has(candidate))) {
+ if (!candidates.some((candidate) => index.relAllFiles.has(candidate))) {
broken.push({
file: rel,
line: lineNum + 1,
@@ -411,13 +472,16 @@ export function auditDocsLinks() {
}
}
- for (const page of collectNavPageEntries(docsConfig.navigation || [])) {
+ for (const page of collectNavPageEntries(index.docsConfig.navigation || [])) {
if (isGeneratedTranslatedDoc(page)) {
continue;
}
checked++;
const route = normalizeRoute(page);
- const resolvedRoute = resolveRoute(route);
+ const resolvedRoute = resolveRoute(route, {
+ redirects: index.redirects,
+ routes: index.routes,
+ });
if (resolvedRoute.ok) {
continue;
}
@@ -451,7 +515,8 @@ export function runDocsLinkAuditCli(options = {}) {
const cleanupAnchorAuditDocsDirImpl =
options.cleanupAnchorAuditDocsDirImpl ??
((dir) => fs.rmSync(dir, { recursive: true, force: true }));
- const anchorDocsDir = prepareAnchorAuditDocsDirImpl(DOCS_DIR);
+ const mirroredDocsDir = prepareMirroredDocsDir(DOCS_DIR);
+ const anchorDocsDir = prepareAnchorAuditDocsDirImpl(mirroredDocsDir.dir);
try {
// Use the npm Mintlify package explicitly. Some developer machines also
@@ -470,18 +535,27 @@ export function runDocsLinkAuditCli(options = {}) {
return result.status ?? 1;
} finally {
cleanupAnchorAuditDocsDirImpl(anchorDocsDir);
+ mirroredDocsDir.cleanup();
}
}
- const { checked, broken } = auditDocsLinks();
- console.log(`checked_internal_links=${checked}`);
- console.log(`broken_links=${broken.length}`);
+ const mirroredDocsDir = prepareMirroredDocsDir(DOCS_DIR);
+ try {
+ const { checked, broken } = auditDocsLinks({
+ docsDir: mirroredDocsDir.dir,
+ allowExternalClawHubRoutes: !mirroredDocsDir.mirroredClawHub,
+ });
+ console.log(`checked_internal_links=${checked}`);
+ console.log(`broken_links=${broken.length}`);
- for (const item of broken) {
- console.log(`${item.file}:${item.line} :: ${item.link} :: ${item.reason}`);
+ for (const item of broken) {
+ console.log(`${item.file}:${item.line} :: ${item.link} :: ${item.reason}`);
+ }
+
+ return broken.length > 0 ? 1 : 0;
+ } finally {
+ mirroredDocsDir.cleanup();
}
-
- return broken.length > 0 ? 1 : 0;
}
function isCliEntry() {
diff --git a/scripts/docs-sync-publish.mjs b/scripts/docs-sync-publish.mjs
index 929329f0067..63a3f69aa71 100644
--- a/scripts/docs-sync-publish.mjs
+++ b/scripts/docs-sync-publish.mjs
@@ -3,13 +3,20 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
-import { fileURLToPath } from "node:url";
+import { fileURLToPath, pathToFileURL } from "node:url";
import { repairMintlifyAccordionIndentation } from "./lib/mintlify-accordion.mjs";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, "..");
const SOURCE_DOCS_DIR = path.join(ROOT, "docs");
const SOURCE_CONFIG_PATH = path.join(SOURCE_DOCS_DIR, "docs.json");
+const DEFAULT_CLAWHUB_SOURCE_REPO = "openclaw/clawhub";
+const CLAWHUB_DOCS_TARGET_DIR = "clawhub";
+const CLAWHUB_REPO_ENV = "OPENCLAW_DOCS_SYNC_CLAWHUB_REPO";
+const DEFAULT_CLAWHUB_REPO_CANDIDATES = [
+ path.resolve(ROOT, "..", "clawhub-docs-clawhub"),
+ path.resolve(ROOT, "..", "clawhub"),
+];
const SYNC_SUPPORT_FILES = [
{
source: path.join(ROOT, "scripts", "check-docs-mdx.mjs"),
@@ -166,6 +173,10 @@ function parseArgs(argv) {
target: "",
sourceRepo: "",
sourceSha: "",
+ clawhubRepo: process.env[CLAWHUB_REPO_ENV] || "",
+ clawhubSourceRepo:
+ process.env.OPENCLAW_DOCS_SYNC_CLAWHUB_SOURCE_REPO || DEFAULT_CLAWHUB_SOURCE_REPO,
+ clawhubSourceSha: process.env.OPENCLAW_DOCS_SYNC_CLAWHUB_SOURCE_SHA || "",
};
for (let index = 0; index < argv.length; index += 1) {
@@ -183,6 +194,18 @@ function parseArgs(argv) {
args.sourceSha = argv[index + 1] ?? "";
index += 1;
break;
+ case "--clawhub-repo":
+ args.clawhubRepo = argv[index + 1] ?? "";
+ index += 1;
+ break;
+ case "--clawhub-source-repo":
+ args.clawhubSourceRepo = argv[index + 1] ?? "";
+ index += 1;
+ break;
+ case "--clawhub-source-sha":
+ args.clawhubSourceSha = argv[index + 1] ?? "";
+ index += 1;
+ break;
default:
throw new Error(`unknown arg: ${part}`);
}
@@ -207,6 +230,30 @@ function ensureDir(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
}
+function normalizeSlashes(value) {
+ return value.replace(/\\/g, "/");
+}
+
+function walkFiles(entryPath, out = []) {
+ if (!fs.existsSync(entryPath)) {
+ return out;
+ }
+
+ const stat = fs.statSync(entryPath);
+ if (stat.isFile()) {
+ out.push(entryPath);
+ return out;
+ }
+
+ for (const entry of fs.readdirSync(entryPath, { withFileTypes: true })) {
+ if (entry.name === "node_modules" || entry.name === ".git") {
+ continue;
+ }
+ walkFiles(path.join(entryPath, entry.name), out);
+ }
+ return out;
+}
+
function walkMarkdownFiles(entryPath, out = []) {
if (!fs.existsSync(entryPath)) {
return out;
@@ -238,6 +285,39 @@ function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}
+function getGitHeadSha(repoPath) {
+ try {
+ return execFileSync("git", ["-C", repoPath, "rev-parse", "HEAD"], {
+ cwd: ROOT,
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "ignore"],
+ }).trim();
+ } catch {
+ return "";
+ }
+}
+
+export function resolveClawHubRepoPath(value = "", options = {}) {
+ const required = options.required !== false;
+ const candidates = [
+ value,
+ process.env[CLAWHUB_REPO_ENV] || "",
+ ...DEFAULT_CLAWHUB_REPO_CANDIDATES,
+ ].filter((candidate) => candidate.trim().length > 0);
+
+ for (const candidate of candidates) {
+ const repoPath = path.resolve(candidate);
+ if (fs.existsSync(path.join(repoPath, "docs"))) {
+ return repoPath;
+ }
+ }
+
+ if (required) {
+ throw new Error(`missing ClawHub docs source; pass --clawhub-repo or set ${CLAWHUB_REPO_ENV}`);
+ }
+ return "";
+}
+
function prefixLocalePage(entry, localeDir) {
if (typeof entry === "string") {
return `${localeDir}/${entry}`;
@@ -259,6 +339,25 @@ function prefixLocalePage(entry, localeDir) {
return clone;
}
+function prefixLocaleNavGroup(group, localeDir) {
+ const clone = { ...group };
+ if (Array.isArray(clone.pages)) {
+ clone.pages = clone.pages.map((entry) => prefixLocalePage(entry, localeDir));
+ }
+ return clone;
+}
+
+function prefixLocaleNavTab(tab, localeDir) {
+ const clone = { ...tab };
+ if (Array.isArray(clone.pages)) {
+ clone.pages = clone.pages.map((entry) => prefixLocalePage(entry, localeDir));
+ }
+ if (Array.isArray(clone.groups)) {
+ clone.groups = clone.groups.map((group) => prefixLocaleNavGroup(group, localeDir));
+ }
+ return clone;
+}
+
function cloneEnglishLanguageNav(englishNav, locale) {
if (!englishNav) {
throw new Error("docs/docs.json is missing navigation.languages.en");
@@ -267,20 +366,9 @@ function cloneEnglishLanguageNav(englishNav, locale) {
...englishNav,
language: locale.language,
tabs: Array.isArray(englishNav.tabs)
- ? englishNav.tabs.map((tab) => ({
- ...tab,
- pages: Array.isArray(tab.pages)
- ? tab.pages.map((entry) => prefixLocalePage(entry, locale.dir))
- : tab.pages,
- groups: Array.isArray(tab.groups)
- ? tab.groups.map((group) => ({
- ...group,
- pages: Array.isArray(group.pages)
- ? group.pages.map((entry) => prefixLocalePage(entry, locale.dir))
- : group.pages,
- }))
- : tab.groups,
- }))
+ ? englishNav.tabs
+ .filter((tab) => tab?.tab !== "ClawHub")
+ .map((tab) => prefixLocaleNavTab(tab, locale.dir))
: englishNav.tabs,
};
}
@@ -370,7 +458,169 @@ function repairGeneratedLocaleDocs(targetDocsDir) {
}
}
-function syncDocsTree(targetRoot) {
+function shouldExcludeClawHubDocsPath(relativePath) {
+ const normalized = normalizeSlashes(relativePath);
+ return (
+ normalized === "specs" || normalized.startsWith("specs/") || normalized.includes("/specs/")
+ );
+}
+
+function toClawHubTargetRelativePath(relativePath) {
+ const normalized = normalizeSlashes(relativePath);
+ if (normalized === "README.md") {
+ return "";
+ }
+ if (normalized === "clawhub.md") {
+ return "index.md";
+ }
+ return normalized.replace(/\/README\.md$/iu, "/index.md");
+}
+
+function toClawHubDocsRoute(relativePath) {
+ const targetRelativePath = toClawHubTargetRelativePath(relativePath);
+ if (!targetRelativePath) {
+ return "";
+ }
+
+ const normalized = targetRelativePath.replace(/\.mdx?$/iu, "");
+ if (normalized === "index") {
+ return `/${CLAWHUB_DOCS_TARGET_DIR}`;
+ }
+ if (normalized.endsWith("/index")) {
+ return `/${CLAWHUB_DOCS_TARGET_DIR}/${normalized.slice(0, -"/index".length)}`;
+ }
+ return `/${CLAWHUB_DOCS_TARGET_DIR}/${normalized}`;
+}
+
+function splitLinkTarget(value) {
+ const match = /^(\S+)(.*)$/su.exec(value);
+ return {
+ target: match?.[1] ?? value,
+ suffix: match?.[2] ?? "",
+ };
+}
+
+function splitTargetParts(value) {
+ const hashIndex = value.indexOf("#");
+ const queryIndex = value.indexOf("?");
+ const splitIndexes = [hashIndex, queryIndex].filter((index) => index >= 0);
+ const splitIndex = splitIndexes.length > 0 ? Math.min(...splitIndexes) : -1;
+ if (splitIndex === -1) {
+ return { pathPart: value, rest: "" };
+ }
+ return {
+ pathPart: value.slice(0, splitIndex),
+ rest: value.slice(splitIndex),
+ };
+}
+
+function rewriteClawHubMarkdownLinkTarget(rawTarget, relativeSourceDir, source) {
+ const { target, suffix } = splitLinkTarget(rawTarget);
+ if (/^(?:https?:|mailto:|tel:|data:|#)/iu.test(target) || target.startsWith("/")) {
+ return rawTarget;
+ }
+
+ const { pathPart, rest } = splitTargetParts(target);
+ if (!pathPart) {
+ return rawTarget;
+ }
+
+ let normalizedRelative = "";
+ if (pathPart.startsWith("docs/")) {
+ normalizedRelative = normalizeSlashes(pathPart.slice("docs/".length));
+ } else if (
+ pathPart.startsWith("./") ||
+ pathPart.startsWith("../") ||
+ /\.mdx?$/iu.test(pathPart)
+ ) {
+ normalizedRelative = normalizeSlashes(path.normalize(path.join(relativeSourceDir, pathPart)));
+ } else {
+ return rawTarget;
+ }
+
+ if (normalizedRelative.startsWith("../")) {
+ const sourceRef = source.sha || "main";
+ const repoRelative = normalizeSlashes(
+ path.normalize(path.join("docs", relativeSourceDir, pathPart)),
+ ).replace(/^(?:\.\.\/)+/u, "");
+ return `https://github.com/${source.repository}/blob/${sourceRef}/${repoRelative}${rest}${suffix}`;
+ }
+
+ if (!/\.mdx?$/iu.test(normalizedRelative)) {
+ return rawTarget;
+ }
+
+ const route = toClawHubDocsRoute(normalizedRelative);
+ return route ? `${route}${rest}${suffix}` : rawTarget;
+}
+
+function rewriteClawHubMarkdownLinks(raw, relativeSourcePath, source) {
+ const relativeSourceDir = normalizeSlashes(path.dirname(relativeSourcePath));
+ const baseDir = relativeSourceDir === "." ? "" : relativeSourceDir;
+ return raw.replace(/(!?\[[^\]]*\]\()([^)]+)(\))/gu, (_match, prefix, target, suffix) => {
+ return `${prefix}${rewriteClawHubMarkdownLinkTarget(target, baseDir, source)}${suffix}`;
+ });
+}
+
+export function syncClawHubDocsTree(targetDocsDir, options = {}) {
+ const repoPath = resolveClawHubRepoPath(options.repoPath || "", {
+ required: options.required !== false,
+ });
+ if (!repoPath) {
+ return {
+ repository: options.sourceRepo || DEFAULT_CLAWHUB_SOURCE_REPO,
+ sha: options.sourceSha || "",
+ path: "",
+ files: 0,
+ };
+ }
+
+ const sourceDocsDir = path.join(repoPath, "docs");
+ const targetDir = path.join(targetDocsDir, CLAWHUB_DOCS_TARGET_DIR);
+ const source = {
+ repository: options.sourceRepo || DEFAULT_CLAWHUB_SOURCE_REPO,
+ sha: options.sourceSha || getGitHeadSha(repoPath),
+ };
+
+ fs.rmSync(targetDir, { recursive: true, force: true });
+ ensureDir(targetDir);
+
+ let copied = 0;
+ for (const sourcePath of walkFiles(sourceDocsDir)) {
+ const relativeSourcePath = normalizeSlashes(path.relative(sourceDocsDir, sourcePath));
+ if (shouldExcludeClawHubDocsPath(relativeSourcePath)) {
+ continue;
+ }
+
+ const targetRelativePath = toClawHubTargetRelativePath(relativeSourcePath);
+ if (!targetRelativePath) {
+ continue;
+ }
+ const targetPath = path.join(targetDir, targetRelativePath);
+ ensureDir(path.dirname(targetPath));
+
+ if (/\.mdx?$/iu.test(sourcePath)) {
+ const raw = fs.readFileSync(sourcePath, "utf8");
+ fs.writeFileSync(
+ targetPath,
+ rewriteClawHubMarkdownLinks(raw, relativeSourcePath, source),
+ "utf8",
+ );
+ } else {
+ fs.copyFileSync(sourcePath, targetPath);
+ }
+ copied += 1;
+ }
+
+ console.log(`Synced ${copied} ClawHub doc asset(s) from ${repoPath}.`);
+ return {
+ ...source,
+ path: repoPath,
+ files: copied,
+ };
+}
+
+function syncDocsTree(targetRoot, options = {}) {
const targetDocsDir = path.join(targetRoot, "docs");
ensureDir(targetDocsDir);
@@ -406,15 +656,32 @@ function syncDocsTree(targetRoot) {
}
}
+ const clawhubSource = syncClawHubDocsTree(targetDocsDir, {
+ repoPath: options.clawhubRepo,
+ sourceRepo: options.clawhubSourceRepo,
+ sourceSha: options.clawhubSourceSha,
+ });
pruneOrphanLocaleDocs(targetDocsDir);
repairGeneratedLocaleDocs(targetDocsDir);
writeJson(path.join(targetDocsDir, "docs.json"), composeDocsConfig());
+ return { clawhub: clawhubSource };
}
-function writeSyncMetadata(targetRoot, args) {
+function writeSyncMetadata(targetRoot, args, sources) {
const metadata = {
repository: args.sourceRepo || "",
sha: args.sourceSha || "",
+ sources: {
+ openclaw: {
+ repository: args.sourceRepo || "",
+ sha: args.sourceSha || "",
+ },
+ clawhub: {
+ repository:
+ sources.clawhub.repository || args.clawhubSourceRepo || DEFAULT_CLAWHUB_SOURCE_REPO,
+ sha: sources.clawhub.sha || args.clawhubSourceSha || "",
+ },
+ },
syncedAt: new Date().toISOString(),
};
writeJson(path.join(targetRoot, ".openclaw-sync", "source.json"), metadata);
@@ -436,9 +703,21 @@ function main() {
throw new Error(`target does not exist: ${targetRoot}`);
}
- syncDocsTree(targetRoot);
+ const clawhubRepo = resolveClawHubRepoPath(args.clawhubRepo);
+ const sources = syncDocsTree(targetRoot, {
+ clawhubRepo,
+ clawhubSourceRepo: args.clawhubSourceRepo,
+ clawhubSourceSha: args.clawhubSourceSha,
+ });
syncSupportFiles(targetRoot);
- writeSyncMetadata(targetRoot, args);
+ writeSyncMetadata(targetRoot, args, sources);
}
-main();
+function isCliEntry() {
+ const cliArg = process.argv[1];
+ return cliArg ? import.meta.url === pathToFileURL(cliArg).href : false;
+}
+
+if (isCliEntry()) {
+ main();
+}
diff --git a/scripts/github/barnacle-auto-response.mjs b/scripts/github/barnacle-auto-response.mjs
index 77164a94893..c11fb84a351 100644
--- a/scripts/github/barnacle-auto-response.mjs
+++ b/scripts/github/barnacle-auto-response.mjs
@@ -13,7 +13,7 @@ import {
const activePrLimit = 20;
const thirdPartyExtensionMessage =
- "Please publish this as a third-party plugin on [ClawHub](https://clawhub.ai) instead of adding it to the core repo. Docs: https://docs.openclaw.ai/plugin and https://docs.openclaw.ai/tools/clawhub";
+ "Please publish this as a third-party plugin on [ClawHub](https://clawhub.ai) instead of adding it to the core repo. Docs: https://docs.openclaw.ai/plugin and https://docs.openclaw.ai/clawhub";
const rules = [
{