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