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:
Patrick Erichsen
2026-05-07 18:55:08 -07:00
committed by GitHub
parent 478996231a
commit 10f9a758b6
21 changed files with 511 additions and 571 deletions
+10
View File
@@ -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:
+15 -1
View File
@@ -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
-1
View File
@@ -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"
]
},
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
+1 -1
View File
@@ -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 = [
{