mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
docs: add dedicated ClawHub docs tab (#79159)
* docs: add clawhub docs tab * fix: satisfy docs sync lint * docs: prune internal clawhub nav pages * docs: include ClawHub publishing page in nav * docs: use clawhub how-it-works route
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Hook packs and npm specs">
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+35
-6
@@ -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": [
|
||||
|
||||
+1
-1
@@ -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/<name>/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/<name>/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).
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -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:<package-name>`. Bare package specs still
|
||||
install from npm during the launch cutover.
|
||||
|
||||
|
||||
@@ -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.
|
||||
<Steps>
|
||||
<Step title="Publish to ClawHub or npm">
|
||||
Your plugin must be installable via `openclaw plugins install \<package-name\>`.
|
||||
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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="npm">
|
||||
|
||||
@@ -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)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+3
-468
@@ -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
|
||||
|
||||
<Steps>
|
||||
<Step title="Search">
|
||||
```bash
|
||||
openclaw skills search "calendar"
|
||||
```
|
||||
</Step>
|
||||
<Step title="Install">
|
||||
```bash
|
||||
openclaw skills install <skill-slug>
|
||||
```
|
||||
</Step>
|
||||
<Step title="Use">
|
||||
Start a new OpenClaw session - it picks up the new skill.
|
||||
</Step>
|
||||
<Step title="Publish (optional)">
|
||||
For registry-authenticated workflows (publish, sync, manage), install
|
||||
the separate `clawhub` CLI:
|
||||
|
||||
```bash
|
||||
npm i -g clawhub
|
||||
# or
|
||||
pnpm add -g clawhub
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Native OpenClaw flows
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Skills">
|
||||
```bash
|
||||
openclaw skills search "calendar"
|
||||
openclaw skills install <skill-slug>
|
||||
openclaw skills update --all
|
||||
```
|
||||
|
||||
Native `openclaw` commands install into your active workspace and
|
||||
persist source metadata so later `update` calls can stay on ClawHub.
|
||||
|
||||
</Tab>
|
||||
<Tab title="Plugins">
|
||||
```bash
|
||||
openclaw plugins search "calendar"
|
||||
openclaw plugins install clawhub:<package>
|
||||
openclaw plugins update --all
|
||||
```
|
||||
|
||||
`plugins search` queries the ClawHub plugin catalog and prints install-ready
|
||||
package names. Use `clawhub:<package>` 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:<package>` 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.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Note>
|
||||
`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 <slug>` 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.
|
||||
</Note>
|
||||
|
||||
## 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
|
||||
`<workspace>/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.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Security scans">
|
||||
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`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Reporting">
|
||||
- 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.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Moderation">
|
||||
- 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.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## ClawHub CLI
|
||||
|
||||
You only need this for registry-authenticated workflows such as
|
||||
publish/sync.
|
||||
|
||||
### Global options
|
||||
|
||||
<ParamField path="--workdir <dir>" type="string">
|
||||
Working directory. Default: current dir; falls back to OpenClaw workspace.
|
||||
</ParamField>
|
||||
<ParamField path="--dir <dir>" type="string" default="skills">
|
||||
Skills directory, relative to workdir.
|
||||
</ParamField>
|
||||
<ParamField path="--site <url>" type="string">
|
||||
Site base URL (browser login).
|
||||
</ParamField>
|
||||
<ParamField path="--registry <url>" type="string">
|
||||
Registry API base URL.
|
||||
</ParamField>
|
||||
<ParamField path="--no-input" type="boolean">
|
||||
Disable prompts (non-interactive).
|
||||
</ParamField>
|
||||
<ParamField path="-V, --cli-version" type="boolean">
|
||||
Print CLI version.
|
||||
</ParamField>
|
||||
|
||||
### Commands
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Auth (login / logout / whoami)">
|
||||
```bash
|
||||
clawhub login # browser flow
|
||||
clawhub login --token <token>
|
||||
clawhub logout
|
||||
clawhub whoami
|
||||
```
|
||||
|
||||
Login options:
|
||||
|
||||
- `--token <token>` - paste an API token.
|
||||
- `--label <label>` - label stored for browser login tokens (default: `CLI token`).
|
||||
- `--no-browser` - do not open a browser (requires `--token`).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Search">
|
||||
```bash
|
||||
clawhub search "query"
|
||||
```
|
||||
|
||||
Searches skills. For plugin/package discovery, use `clawhub package explore`.
|
||||
|
||||
- `--limit <n>` - max results.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Browse / inspect plugins">
|
||||
```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:<package>`.
|
||||
|
||||
Options:
|
||||
|
||||
- `--family skill|code-plugin|bundle-plugin` - filter package family.
|
||||
- `--official` - show only official packages.
|
||||
- `--executes-code` - show only packages that execute code.
|
||||
- `--version <version>` / `--tag <tag>` - inspect a specific package version.
|
||||
- `--versions`, `--files`, `--file <path>` - inspect package history and files.
|
||||
- `--json` - machine-readable output.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Install / update / list">
|
||||
```bash
|
||||
clawhub install <slug>
|
||||
clawhub update <slug>
|
||||
clawhub update --all
|
||||
clawhub list
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `--version <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`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Publish skills">
|
||||
```bash
|
||||
clawhub skill publish <path>
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `--slug <slug>` - skill slug.
|
||||
- `--name <name>` - display name.
|
||||
- `--version <version>` - semver version.
|
||||
- `--changelog <text>` - changelog text (can be empty).
|
||||
- `--tags <tags>` - comma-separated tags (default: `latest`).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Publish plugins">
|
||||
```bash
|
||||
clawhub package publish <source>
|
||||
```
|
||||
|
||||
`<source>` 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.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Request rescans">
|
||||
```bash
|
||||
clawhub skill rescan <slug>
|
||||
clawhub skill rescan <slug> --yes --json
|
||||
|
||||
clawhub package rescan <name>
|
||||
clawhub package rescan <name> --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.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Delete / undelete (owner or admin)">
|
||||
```bash
|
||||
clawhub delete <slug> --yes
|
||||
clawhub undelete <slug> --yes
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="Sync (scan local + publish new or updated)">
|
||||
```bash
|
||||
clawhub sync
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `--root <dir...>` - extra scan roots.
|
||||
- `--all` - upload everything without prompts.
|
||||
- `--dry-run` - show what would be uploaded.
|
||||
- `--bump <type>` - `patch|minor|major` for updates (default: `patch`).
|
||||
- `--changelog <text>` - changelog for non-interactive updates.
|
||||
- `--tags <tags>` - comma-separated tags (default: `latest`).
|
||||
- `--concurrency <n>` - registry checks (default: `4`).
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Common workflows
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Search">
|
||||
```bash
|
||||
clawhub search "postgres backups"
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Find a plugin">
|
||||
```bash
|
||||
clawhub package explore --family code-plugin
|
||||
clawhub package explore "memory" --family code-plugin
|
||||
clawhub package inspect episodic-claw
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Install">
|
||||
```bash
|
||||
clawhub install my-skill-pack
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Update all">
|
||||
```bash
|
||||
clawhub update --all
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Publish a single skill">
|
||||
```bash
|
||||
clawhub skill publish ./my-skill --slug my-skill --name "My Skill" --version 1.0.0 --tags latest
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Sync many skills">
|
||||
```bash
|
||||
clawhub sync --all
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Publish a plugin from GitHub">
|
||||
```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
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 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
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Versioning and tags">
|
||||
- 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.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Local changes vs registry versions">
|
||||
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).
|
||||
</Accordion>
|
||||
<Accordion title="Sync scanning and fallback roots">
|
||||
`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.
|
||||
</Accordion>
|
||||
<Accordion title="Storage and lockfile">
|
||||
- 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`).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Telemetry (install counts)">
|
||||
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
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
+129
-55
@@ -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<string, string>, routes?: Set<string>}} [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() {
|
||||
|
||||
+299
-20
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user