Compare commits

...

8 Commits

Author SHA1 Message Date
Jim Liu 宝玉 1cb54420e0 chore: release v1.59.1 2026-03-11 17:08:33 -05:00
Jim Liu 宝玉 6363bd83e2 refactor(scripts): replace clawhub CLI with local sync-clawhub.mjs 2026-03-11 16:45:57 -05:00
Jim Liu 宝玉 7b8247544d refactor(baoyu-post-to-weibo): add entry point guard to md-to-html.ts 2026-03-11 16:45:56 -05:00
Jim Liu 宝玉 a9576ebc67 fix(baoyu-post-to-x): remove disable-blink-features Chrome flag 2026-03-11 16:45:55 -05:00
Jim Liu 宝玉 79ca378229 fix(baoyu-translate): improve annotation density and style preset passing
- Add short text (<5 sentences) annotation reduction rule to translator's notes
- Explicitly pass resolved style preset to 02-prompt.md assembly in all modes
2026-03-11 16:45:51 -05:00
Jim Liu 宝玉 e1a1fe23cb Merge pull request #77 from jackL1020/claude/claude-md-mmlh0po6953zhhbf-D8rFo
docs: Update CLAUDE.md with v1.59.0 features and skill registry
2026-03-11 13:40:27 -05:00
Claude 3d85a7e663 docs: fix marketplace.json path (it's in .claude-plugin/, not root)
https://claude.ai/code/session_01DREj1A7KKhNVE1RXjmg9ZH
2026-03-11 03:28:56 +00:00
Claude 10aabb39f8 docs: update CLAUDE.md to reflect v1.59.0 codebase state
- Add 8 missing skills to architecture tree (baoyu-post-to-weibo,
  baoyu-infographic, baoyu-image-gen, baoyu-url-to-markdown,
  baoyu-format-markdown, baoyu-markdown-to-html, baoyu-translate)
- Fix marketplace.json path (root, not .claude-plugin/)
- Rename SKILL_DIR → baseDir in Script Directory template (v1.57.0 change)
- Add ClawHub/OpenClaw publishing section with sync-clawhub.sh usage
- Add openclaw frontmatter template and requirement for new skills
- Document baoyu-image-gen batch parallel generation capability
- Update Project Overview to mention all AI providers
- Expand Key Dependencies and Choosing a Category tables
- Add image-gen API key requirement note

https://claude.ai/code/session_01DREj1A7KKhNVE1RXjmg9ZH
2026-03-11 03:28:14 +00:00
13 changed files with 659 additions and 43 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
}, },
"metadata": { "metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency", "description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.59.0" "version": "1.59.1"
}, },
"plugins": [ "plugins": [
{ {
+13
View File
@@ -2,6 +2,19 @@
English | [中文](./CHANGELOG.zh.md) English | [中文](./CHANGELOG.zh.md)
## 1.59.1 - 2026-03-11
### Fixes
- `baoyu-translate`: improve short text annotation density rule and add explicit style preset passing to 02-prompt.md
- `baoyu-post-to-x`: remove `--disable-blink-features=AutomationControlled` Chrome flag
### Refactor
- `baoyu-post-to-weibo`: add entry point guard to md-to-html.ts for module import compatibility
- Replace clawhub CLI with local sync-clawhub.mjs script
### Documentation
- Update CLAUDE.md to reflect v1.59.0 codebase state (by @jackL1020)
## 1.59.0 - 2026-03-09 ## 1.59.0 - 2026-03-09
### Features ### Features
+13
View File
@@ -2,6 +2,19 @@
[English](./CHANGELOG.md) | 中文 [English](./CHANGELOG.md) | 中文
## 1.59.1 - 2026-03-11
### 修复
- `baoyu-translate`:改进短文本注释密度规则,补充风格预设到 02-prompt.md 的显式传递
- `baoyu-post-to-x`:移除 `--disable-blink-features=AutomationControlled` Chrome 启动参数
### 重构
- `baoyu-post-to-weibo`:为 md-to-html.ts 添加入口守卫,支持模块导入
- 使用本地 sync-clawhub.mjs 脚本替代 clawhub CLI
### 文档
- 更新 CLAUDE.md 以反映 v1.59.0 代码库状态 (by @jackL1020)
## 1.59.0 - 2026-03-09 ## 1.59.0 - 2026-03-09
### 新功能 ### 新功能
+91 -22
View File
@@ -4,11 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
Claude Code marketplace plugin providing AI-powered content generation skills. Skills use Gemini Web API (reverse-engineered) for text/image generation and Chrome CDP for browser automation. Claude Code marketplace plugin providing AI-powered content generation skills. Current version: **1.59.1**. Skills use official AI APIs (OpenAI, Google, DashScope, Replicate) or the reverse-engineered Gemini Web API for text/image generation, and Chrome CDP for browser automation.
## Architecture ## Architecture
Skills are organized into three plugin categories in `marketplace.json`: Skills are organized into three plugin categories in `.claude-plugin/marketplace.json`:
``` ```
skills/ skills/
@@ -19,16 +19,27 @@ skills/
│ ├── baoyu-article-illustrator/ # Smart illustration placement │ ├── baoyu-article-illustrator/ # Smart illustration placement
│ ├── baoyu-comic/ # Knowledge comics (Logicomix/Ohmsha style) │ ├── baoyu-comic/ # Knowledge comics (Logicomix/Ohmsha style)
│ ├── baoyu-post-to-x/ # X/Twitter posting automation │ ├── baoyu-post-to-x/ # X/Twitter posting automation
── baoyu-post-to-wechat/ # WeChat Official Account posting ── baoyu-post-to-wechat/ # WeChat Official Account posting
│ ├── baoyu-post-to-weibo/ # Weibo posting (text, images, videos, articles)
│ └── baoyu-infographic/ # Professional infographics (21 layouts × 20 styles)
├── [ai-generation-skills] # AI-powered generation backends ├── [ai-generation-skills] # AI-powered generation backends
── baoyu-danger-gemini-web/ # Gemini API wrapper (text + image gen) ── baoyu-danger-gemini-web/ # Gemini Web API wrapper (text + image gen)
│ └── baoyu-image-gen/ # Official API image gen (OpenAI, Google, DashScope, Replicate)
└── [utility-skills] # Utility tools for content processing └── [utility-skills] # Utility tools for content processing
├── baoyu-danger-x-to-markdown/ # X/Twitter content to markdown ├── baoyu-danger-x-to-markdown/ # X/Twitter content to markdown
── baoyu-compress-image/ # Image compression ── baoyu-compress-image/ # Image compression (WebP/PNG via sips/cwebp/Sharp)
├── baoyu-url-to-markdown/ # Fetch any URL → markdown via Chrome CDP
├── baoyu-format-markdown/ # Format/beautify plain text or markdown files
├── baoyu-markdown-to-html/ # Markdown → styled HTML (WeChat-compatible themes)
└── baoyu-translate/ # Multi-mode translation (quick/normal/refined)
``` ```
Top-level `scripts/` directory contains repository maintenance utilities:
- `scripts/sync-clawhub.sh` - Publish skills to ClawHub/OpenClaw registry
- `scripts/sync-md-to-wechat.sh` - Sync markdown content to WeChat
**Plugin Categories**: **Plugin Categories**:
| Category | Description | | Category | Description |
|----------|-------------| |----------|-------------|
@@ -37,9 +48,10 @@ skills/
| `utility-skills` | Helper tools for content processing (conversion, compression) | | `utility-skills` | Helper tools for content processing (conversion, compression) |
Each skill contains: Each skill contains:
- `SKILL.md` - YAML front matter (name, description) + documentation - `SKILL.md` - YAML front matter (name, description, version, openclaw metadata) + documentation
- `scripts/` - TypeScript implementations - `scripts/` - TypeScript implementations (optional, pure-prompt skills omit this)
- `prompts/system.md` - AI generation guidelines (optional) - `references/` - Extended reference files linked from SKILL.md (optional)
- `prompts/` - AI prompt templates (optional, used by some content skills)
## Running Skills ## Running Skills
@@ -88,8 +100,9 @@ ${BUN_X} skills/baoyu-danger-gemini-web/scripts/main.ts --promptfiles system.md
## Key Dependencies ## Key Dependencies
- **Bun**: TypeScript runtime (native `bun` preferred, fallback `npx -y bun`) - **Bun**: TypeScript runtime (native `bun` preferred, fallback `npx -y bun`)
- **Chrome**: Required for `baoyu-danger-gemini-web` auth and `baoyu-post-to-x` automation - **Chrome**: Required for `baoyu-danger-gemini-web` auth, `baoyu-post-to-x`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, and `baoyu-url-to-markdown`
- **npm packages (per skill)**: Some skill subprojects include `package.json`/lockfiles and require dependency installation in their own `scripts/` directories - **npm packages (per skill)**: Some skill subprojects include `package.json`/lockfiles and require dependency installation in their own `scripts/` directories
- **Image generation APIs**: `baoyu-image-gen` requires at least one API key (OpenAI, Google, DashScope, or Replicate)
## Chrome Profile (Unified) ## Chrome Profile (Unified)
@@ -166,10 +179,40 @@ Skills that process external Markdown/HTML should treat content as untrusted:
- Cookies cached in data directory - Cookies cached in data directory
- Force refresh: `--login` flag - Force refresh: `--login` flag
`baoyu-image-gen` uses official API keys configured in EXTEND.md (loaded at Step 0 before generation).
## Plugin Configuration ## Plugin Configuration
`.claude-plugin/marketplace.json` defines plugin metadata and skill paths. Version follows semver. `.claude-plugin/marketplace.json` defines plugin metadata and skill paths. Version follows semver.
The file contains:
- `name`: Plugin name (`baoyu-skills`)
- `owner`: Author info
- `metadata.version`: Semver version (currently `1.59.1`)
- `plugins[]`: Array of plugin categories, each with `name`, `description`, `source`, `strict`, and `skills[]` paths
## ClawHub / OpenClaw Publishing
Skills include `metadata.openclaw` in their YAML front matter for registry compatibility:
```yaml
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#<skill-name>
requires: # optional — only for skills with scripts
anyBins:
- bun
- npx
```
To publish/sync skills to ClawHub registry:
```bash
bash scripts/sync-clawhub.sh # sync all skills
bash scripts/sync-clawhub.sh <skill> # sync one skill
```
Requires `clawhub` CLI or `npx` (auto-downloads via npx if not installed).
## Skill Loading Rules ## Skill Loading Rules
**IMPORTANT**: When working in this project, follow these rules: **IMPORTANT**: When working in this project, follow these rules:
@@ -212,29 +255,50 @@ Skills that process external Markdown/HTML should treat content as untrusted:
| **References** | Keep one level deep from SKILL.md; avoid nested references | | **References** | Keep one level deep from SKILL.md; avoid nested references |
| **No time-sensitive info** | Avoid dates/versions that become outdated | | **No time-sensitive info** | Avoid dates/versions that become outdated |
### SKILL.md Frontmatter Template
```yaml
---
name: baoyu-<name>
description: <Third-person description. What it does + when to use it.>
version: <semver matching marketplace.json>
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-<name>
requires: # include only if skill has scripts
anyBins:
- bun
- npx
---
```
### Steps ### Steps
1. Create `skills/baoyu-<name>/SKILL.md` with YAML front matter 1. Create `skills/baoyu-<name>/SKILL.md` with YAML front matter (see template above)
- Directory name: `baoyu-<name>` - Directory name: `baoyu-<name>`
- SKILL.md `name` field: `baoyu-<name>` - SKILL.md `name` field: `baoyu-<name>`
2. Add TypeScript in `skills/baoyu-<name>/scripts/` - Always include `metadata.openclaw.homepage`
- Include `metadata.openclaw.requires.anyBins` only when the skill has executable scripts
2. Add TypeScript in `skills/baoyu-<name>/scripts/` (if applicable)
3. Add prompt templates in `skills/baoyu-<name>/prompts/` if needed 3. Add prompt templates in `skills/baoyu-<name>/prompts/` if needed
4. **Choose the appropriate category** and register in `marketplace.json`: 4. **Choose the appropriate category** and register in `marketplace.json`:
- `content-skills`: For content generation/publishing (images, slides, posts) - `content-skills`: For content generation/publishing (images, slides, posts)
- `ai-generation-skills`: For AI backend capabilities - `ai-generation-skills`: For AI backend capabilities
- `utility-skills`: For helper tools (conversion, compression) - `utility-skills`: For helper tools (conversion, compression)
- If none fit, create a new category with descriptive name - If none fit, create a new category with descriptive name
5. **Add Script Directory section** to SKILL.md (see template below) 5. **Add Script Directory section** to SKILL.md (see template below) if skill has scripts
6. **Add openclaw metadata** to SKILL.md frontmatter (required for ClawHub registry)
### Choosing a Category ### Choosing a Category
| If your skill... | Use category | | If your skill... | Use category |
|------------------|--------------| |------------------|--------------|
| Generates visual content (images, slides, comics) | `content-skills` | | Generates visual content (images, slides, comics, infographics) | `content-skills` |
| Publishes to platforms (X, WeChat, etc.) | `content-skills` | | Publishes to platforms (X, WeChat, Weibo, etc.) | `content-skills` |
| Provides AI generation backend | `ai-generation-skills` | | Provides AI generation backend (official API or reverse-engineered) | `ai-generation-skills` |
| Converts or processes content | `utility-skills` | | Converts or processes content (URL→md, md→html, format) | `utility-skills` |
| Compresses or optimizes files | `utility-skills` | | Compresses or optimizes files | `utility-skills` |
| Translates documents | `utility-skills` |
**Creating a new category**: If the skill doesn't fit existing categories, add a new plugin object to `marketplace.json` with: **Creating a new category**: If the skill doesn't fit existing categories, add a new plugin object to `marketplace.json` with:
- `name`: Descriptive kebab-case name (e.g., `analytics-skills`) - `name`: Descriptive kebab-case name (e.g., `analytics-skills`)
@@ -266,10 +330,10 @@ Every SKILL.md with scripts MUST include this section after Usage:
**Important**: All scripts are located in the `scripts/` subdirectory of this skill. **Important**: All scripts are located in the `scripts/` subdirectory of this skill.
**Agent Execution Instructions**: **Agent Execution Instructions**:
1. Determine this SKILL.md file's directory path as `SKILL_DIR` 1. Determine this SKILL.md file's directory path as `{baseDir}`
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts` 2. Script path = `{baseDir}/scripts/<script-name>.ts`
3. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun 3. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
4. Replace all `${SKILL_DIR}` and `${BUN_X}` in this document with actual values 4. Replace all `{baseDir}` and `${BUN_X}` in this document with actual values
**Script Reference**: **Script Reference**:
| Script | Purpose | | Script | Purpose |
@@ -278,7 +342,9 @@ Every SKILL.md with scripts MUST include this section after Usage:
| `scripts/other.ts` | Other functionality | | `scripts/other.ts` | Other functionality |
``` ```
When referencing scripts in workflow sections, use `${BUN_X} ${SKILL_DIR}/scripts/<name>.ts` so agents can resolve the correct runtime and path. When referencing scripts in workflow sections, use `${BUN_X} {baseDir}/scripts/<name>.ts` so agents can resolve the correct runtime and path.
> **Note**: The path variable was renamed from `SKILL_DIR` / `${SKILL_DIR}` to `baseDir` / `{baseDir}` (no `${}` wrapper) in v1.57.0. All existing SKILL.md files use `{baseDir}`.
### Progressive Disclosure ### Progressive Disclosure
@@ -327,7 +393,7 @@ Use this template when implementing image generation in skills:
### Step N: Generate Images ### Step N: Generate Images
**Skill Selection**: **Skill Selection**:
1. Check available image generation skills (e.g., `baoyu-danger-gemini-web`) 1. Check available image generation skills (`baoyu-image-gen` default, or `baoyu-danger-gemini-web`)
2. Read selected skill's SKILL.md for parameter reference 2. Read selected skill's SKILL.md for parameter reference
3. If multiple skills available, ask user to choose 3. If multiple skills available, ask user to choose
@@ -336,11 +402,14 @@ Use this template when implementing image generation in skills:
- Prompt file path (or inline prompt) - Prompt file path (or inline prompt)
- Output image path - Output image path
- Any skill-specific parameters (refer to skill's SKILL.md) - Any skill-specific parameters (refer to skill's SKILL.md)
2. Generate images sequentially (one at a time) 2. Generate images sequentially by default (one at a time)
- Use batch parallel mode only when user already has multiple prompts or explicitly wants parallel throughput
3. After each image, output progress: "Generated X/N" 3. After each image, output progress: "Generated X/N"
4. On failure, auto-retry once before reporting error 4. On failure, auto-retry once before reporting error
``` ```
**Batch Parallel Generation** (`baoyu-image-gen` only): supports concurrent workers with per-provider throttling. Configure via `batch.max_workers` and `batch.provider_limits` in EXTEND.md. Default: sequential.
### Output Path Convention ### Output Path Convention
Each session creates an independent directory. Even the same source file generates a new directory per session. Each session creates an independent directory. Even the same source file generates a new directory per session.
+528
View File
@@ -0,0 +1,528 @@
#!/usr/bin/env node
import crypto from "node:crypto";
import fs from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import os from "node:os";
const DEFAULT_REGISTRY = "https://clawhub.ai";
const TEXT_EXTENSIONS = new Set([
"md",
"mdx",
"txt",
"json",
"json5",
"yaml",
"yml",
"toml",
"js",
"cjs",
"mjs",
"ts",
"tsx",
"jsx",
"py",
"sh",
"rb",
"go",
"rs",
"swift",
"kt",
"java",
"cs",
"cpp",
"c",
"h",
"hpp",
"sql",
"csv",
"ini",
"cfg",
"env",
"xml",
"html",
"css",
"scss",
"sass",
"svg",
]);
async function main() {
const options = parseArgs(process.argv.slice(2));
const config = await readClawhubConfig();
const registry = (
process.env.CLAWHUB_REGISTRY ||
process.env.CLAWDHUB_REGISTRY ||
config.registry ||
DEFAULT_REGISTRY
).replace(/\/+$/, "");
if (!config.token) {
throw new Error("Not logged in. Run: clawhub login");
}
await apiJson(registry, config.token, "/api/v1/whoami");
const roots = options.roots.length > 0 ? options.roots : [path.resolve("skills")];
const skills = await findSkills(roots);
if (skills.length === 0) {
throw new Error("No skills found.");
}
console.log("ClawHub sync");
console.log(`Roots with skills: ${roots.join(", ")}`);
const locals = await mapWithConcurrency(skills, options.concurrency, async (skill) => {
const files = await listTextFiles(skill.folder);
const fingerprint = buildFingerprint(files);
return {
...skill,
fileCount: files.length,
fingerprint,
};
});
const candidates = await mapWithConcurrency(locals, options.concurrency, async (skill) => {
const query = new URLSearchParams({
slug: skill.slug,
hash: skill.fingerprint,
});
const { status, body } = await apiJsonWithStatus(
registry,
config.token,
`/api/v1/resolve?${query.toString()}`
);
if (status === 404) {
return {
...skill,
status: "new",
latestVersion: null,
matchVersion: null,
};
}
if (status !== 200) {
throw new Error(body?.message || `Resolve failed for ${skill.slug} (HTTP ${status})`);
}
const latestVersion = body?.latestVersion?.version ?? null;
const matchVersion = body?.match?.version ?? null;
if (!latestVersion) {
return {
...skill,
status: "new",
latestVersion: null,
matchVersion: null,
};
}
return {
...skill,
status: matchVersion ? "synced" : "update",
latestVersion,
matchVersion,
};
});
const actionable = candidates.filter((candidate) => candidate.status !== "synced");
if (actionable.length === 0) {
console.log("Nothing to sync.");
return;
}
console.log("");
console.log("To sync");
for (const candidate of actionable) {
console.log(`- ${formatCandidate(candidate, options.bump)}`);
}
if (options.dryRun) {
console.log("");
console.log(`Dry run: would upload ${actionable.length} skill(s).`);
return;
}
const tags = options.tags
.split(",")
.map((tag) => tag.trim())
.filter(Boolean);
for (const candidate of actionable) {
const version =
candidate.status === "new"
? "1.0.0"
: bumpSemver(candidate.latestVersion, options.bump);
console.log(`Publishing ${candidate.slug}@${version}`);
const files = await listTextFiles(candidate.folder);
await publishSkill({
registry,
token: config.token,
skill: candidate,
files,
version,
changelog: options.changelog,
tags,
});
}
console.log("");
console.log(`Uploaded ${actionable.length} skill(s).`);
}
function parseArgs(argv) {
const options = {
roots: [],
dryRun: false,
bump: "patch",
changelog: "",
tags: "latest",
concurrency: 4,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--dry-run") {
options.dryRun = true;
continue;
}
if (arg === "--all") {
continue;
}
if (arg === "--root") {
const value = argv[index + 1];
if (!value) throw new Error("--root requires a directory");
options.roots.push(path.resolve(value));
index += 1;
continue;
}
if (arg === "--bump") {
const value = argv[index + 1];
if (!["patch", "minor", "major"].includes(value)) {
throw new Error("--bump must be patch, minor, or major");
}
options.bump = value;
index += 1;
continue;
}
if (arg === "--changelog") {
const value = argv[index + 1];
if (value == null) throw new Error("--changelog requires text");
options.changelog = value;
index += 1;
continue;
}
if (arg === "--tags") {
const value = argv[index + 1];
if (value == null) throw new Error("--tags requires a value");
options.tags = value;
index += 1;
continue;
}
if (arg === "--concurrency") {
const value = Number(argv[index + 1]);
if (!Number.isInteger(value) || value < 1 || value > 32) {
throw new Error("--concurrency must be an integer between 1 and 32");
}
options.concurrency = value;
index += 1;
continue;
}
if (arg === "-h" || arg === "--help") {
printUsage();
process.exit(0);
}
throw new Error(`Unknown argument: ${arg}`);
}
return options;
}
function printUsage() {
console.log(`Usage: sync-clawhub.mjs [options]
Options:
--root <dir> Extra skill root (repeatable)
--all Accepted for compatibility
--dry-run Show what would be uploaded
--bump <type> patch | minor | major
--changelog <text> Changelog for updates
--tags <tags> Comma-separated tags
--concurrency <n> Registry check concurrency (1-32)
-h, --help Show help`);
}
async function readClawhubConfig() {
const configPath = getConfigPath();
try {
return JSON.parse(await fs.readFile(configPath, "utf8"));
} catch {
return {};
}
}
function getConfigPath() {
const override =
process.env.CLAWHUB_CONFIG_PATH?.trim() || process.env.CLAWDHUB_CONFIG_PATH?.trim();
if (override) {
return path.resolve(override);
}
const home = os.homedir();
if (process.platform === "darwin") {
const clawhub = path.join(home, "Library", "Application Support", "clawhub", "config.json");
const clawdhub = path.join(home, "Library", "Application Support", "clawdhub", "config.json");
return pathForExistingConfig(clawhub, clawdhub);
}
const xdg = process.env.XDG_CONFIG_HOME;
if (xdg) {
const clawhub = path.join(xdg, "clawhub", "config.json");
const clawdhub = path.join(xdg, "clawdhub", "config.json");
return pathForExistingConfig(clawhub, clawdhub);
}
if (process.platform === "win32" && process.env.APPDATA) {
const clawhub = path.join(process.env.APPDATA, "clawhub", "config.json");
const clawdhub = path.join(process.env.APPDATA, "clawdhub", "config.json");
return pathForExistingConfig(clawhub, clawdhub);
}
const clawhub = path.join(home, ".config", "clawhub", "config.json");
const clawdhub = path.join(home, ".config", "clawdhub", "config.json");
return pathForExistingConfig(clawhub, clawdhub);
}
function pathForExistingConfig(primary, legacy) {
if (existsSync(primary)) return path.resolve(primary);
if (existsSync(legacy)) return path.resolve(legacy);
return path.resolve(primary);
}
async function findSkills(roots) {
const deduped = new Map();
for (const root of roots) {
const folders = await findSkillFolders(root);
for (const folder of folders) {
deduped.set(folder.slug, folder);
}
}
return [...deduped.values()].sort((left, right) => left.slug.localeCompare(right.slug));
}
async function findSkillFolders(root) {
const stat = await safeStat(root);
if (!stat?.isDirectory()) return [];
if (await hasSkillMarker(root)) {
return [buildSkillEntry(root)];
}
const entries = await fs.readdir(root, { withFileTypes: true });
const found = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const folder = path.join(root, entry.name);
if (await hasSkillMarker(folder)) {
found.push(buildSkillEntry(folder));
}
}
return found;
}
function buildSkillEntry(folder) {
const base = path.basename(folder);
return {
folder,
slug: sanitizeSlug(base),
displayName: titleCase(base),
};
}
async function hasSkillMarker(folder) {
return Boolean(
(await safeStat(path.join(folder, "SKILL.md")))?.isFile() ||
(await safeStat(path.join(folder, "skill.md")))?.isFile()
);
}
async function listTextFiles(root) {
const files = [];
async function walk(folder) {
const entries = await fs.readdir(folder, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".")) continue;
if (entry.name === "node_modules") continue;
if (entry.name === ".clawhub" || entry.name === ".clawdhub") continue;
const fullPath = path.join(folder, entry.name);
if (entry.isDirectory()) {
await walk(fullPath);
continue;
}
if (!entry.isFile()) continue;
const relPath = path.relative(root, fullPath).split(path.sep).join("/");
const ext = relPath.split(".").pop()?.toLowerCase() ?? "";
if (!TEXT_EXTENSIONS.has(ext)) continue;
const bytes = await fs.readFile(fullPath);
files.push({ relPath, bytes });
}
}
await walk(root);
files.sort((left, right) => left.relPath.localeCompare(right.relPath));
return files;
}
function buildFingerprint(files) {
const payload = files
.map((file) => `${file.relPath}:${sha256(file.bytes)}`)
.sort((left, right) => left.localeCompare(right))
.join("\n");
return crypto.createHash("sha256").update(payload).digest("hex");
}
function sha256(bytes) {
return crypto.createHash("sha256").update(bytes).digest("hex");
}
async function publishSkill({ registry, token, skill, files, version, changelog, tags }) {
const form = new FormData();
form.set(
"payload",
JSON.stringify({
slug: skill.slug,
displayName: skill.displayName,
version,
changelog,
tags,
acceptLicenseTerms: true,
})
);
for (const file of files) {
form.append("files", new Blob([file.bytes], { type: "text/plain" }), file.relPath);
}
const response = await fetch(`${registry}/api/v1/skills`, {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
},
body: form,
});
const text = await response.text();
if (!response.ok) {
throw new Error(text || `Publish failed for ${skill.slug} (HTTP ${response.status})`);
}
const result = text ? JSON.parse(text) : {};
console.log(`OK. Published ${skill.slug}@${version}${result.versionId ? ` (${result.versionId})` : ""}`);
}
async function apiJson(registry, token, requestPath) {
const { status, body } = await apiJsonWithStatus(registry, token, requestPath);
if (status < 200 || status >= 300) {
throw new Error(body?.message || `HTTP ${status}`);
}
return body;
}
async function apiJsonWithStatus(registry, token, requestPath) {
const response = await fetch(`${registry}${requestPath}`, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
},
});
const text = await response.text();
let body = null;
try {
body = text ? JSON.parse(text) : null;
} catch {
body = { message: text };
}
return { status: response.status, body };
}
async function mapWithConcurrency(items, limit, fn) {
const results = new Array(items.length);
let cursor = 0;
async function worker() {
while (cursor < items.length) {
const index = cursor;
cursor += 1;
results[index] = await fn(items[index], index);
}
}
const count = Math.min(Math.max(limit, 1), Math.max(items.length, 1));
await Promise.all(Array.from({ length: count }, () => worker()));
return results;
}
function formatCandidate(candidate, bump) {
if (candidate.status === "new") {
return `${candidate.slug} NEW (${candidate.fileCount} files)`;
}
return `${candidate.slug} UPDATE ${candidate.latestVersion} -> ${bumpSemver(
candidate.latestVersion,
bump
)} (${candidate.fileCount} files)`;
}
function bumpSemver(version, bump) {
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version ?? "");
if (!match) {
throw new Error(`Invalid semver: ${version}`);
}
const major = Number(match[1]);
const minor = Number(match[2]);
const patch = Number(match[3]);
if (bump === "major") return `${major + 1}.0.0`;
if (bump === "minor") return `${major}.${minor + 1}.0`;
return `${major}.${minor}.${patch + 1}`;
}
function sanitizeSlug(value) {
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-+/, "")
.replace(/-+$/, "")
.replace(/--+/g, "-");
}
function titleCase(value) {
return value
.trim()
.replace(/[-_]+/g, " ")
.replace(/\s+/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
}
async function safeStat(filePath) {
try {
return await fs.stat(filePath);
} catch {
return null;
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
+3 -8
View File
@@ -4,13 +4,8 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SKILLS_DIR="${ROOT_DIR}/skills" SKILLS_DIR="${ROOT_DIR}/skills"
if command -v clawhub >/dev/null 2>&1; then if ! command -v node >/dev/null 2>&1; then
CLAWHUB_CMD=(clawhub) echo "Error: node is required."
elif command -v npx >/dev/null 2>&1; then
CLAWHUB_CMD=(npx -y clawhub)
else
echo "Error: neither clawhub nor npx is available."
echo "See https://docs.openclaw.ai/zh-CN/tools/clawhub"
exit 1 exit 1
fi fi
@@ -18,4 +13,4 @@ if [ "$#" -eq 0 ]; then
set -- --all set -- --all
fi fi
exec "${CLAWHUB_CMD[@]}" sync --root "${SKILLS_DIR}" "$@" exec node "${ROOT_DIR}/scripts/sync-clawhub.mjs" --root "${SKILLS_DIR}" "$@"
@@ -446,7 +446,9 @@ Options:
} }
} }
await main().catch((err) => { if (import.meta.main ?? (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename ?? ''))) {
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); await main().catch((err) => {
process.exit(1); console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
}); process.exit(1);
});
}
@@ -114,7 +114,6 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
`--user-data-dir=${profileDir}`, `--user-data-dir=${profileDir}`,
'--no-first-run', '--no-first-run',
'--no-default-browser-check', '--no-default-browser-check',
'--disable-blink-features=AutomationControlled',
'--start-maximized', '--start-maximized',
X_ARTICLES_URL, X_ARTICLES_URL,
]; ];
@@ -41,7 +41,6 @@ export async function postToX(options: XBrowserOptions): Promise<void> {
`--user-data-dir=${profileDir}`, `--user-data-dir=${profileDir}`,
'--no-first-run', '--no-first-run',
'--no-default-browser-check', '--no-default-browser-check',
'--disable-blink-features=AutomationControlled',
'--start-maximized', '--start-maximized',
X_COMPOSE_URL, X_COMPOSE_URL,
], { stdio: 'ignore' }); ], { stdio: 'ignore' });
@@ -45,7 +45,6 @@ export async function quotePost(options: QuoteOptions): Promise<void> {
`--user-data-dir=${profileDir}`, `--user-data-dir=${profileDir}`,
'--no-first-run', '--no-first-run',
'--no-default-browser-check', '--no-default-browser-check',
'--disable-blink-features=AutomationControlled',
'--start-maximized', '--start-maximized',
tweetUrl, tweetUrl,
], { stdio: 'ignore' }); ], { stdio: 'ignore' });
@@ -45,7 +45,6 @@ export async function postVideoToX(options: XVideoOptions): Promise<void> {
`--user-data-dir=${profileDir}`, `--user-data-dir=${profileDir}`,
'--no-first-run', '--no-first-run',
'--no-default-browser-check', '--no-default-browser-check',
'--disable-blink-features=AutomationControlled',
'--start-maximized', '--start-maximized',
X_COMPOSE_URL, X_COMPOSE_URL,
], { stdio: 'ignore' }); ], { stdio: 'ignore' });
+3 -3
View File
@@ -188,7 +188,7 @@ Before translating chunks:
- Splits at markdown block boundaries to preserve structure - Splits at markdown block boundaries to preserve structure
- If a single block exceeds the threshold, falls back to line splitting, then word splitting - If a single block exceeds the threshold, falls back to line splitting, then word splitting
4. **Assemble translation prompt**: 4. **Assemble translation prompt**:
- Main agent reads `01-analysis.md` (if exists) and assembles shared context using Part 1 of [references/subagent-prompt-template.md](references/subagent-prompt-template.md) — inlining content background, merged glossary, and comprehension challenges - Main agent reads `01-analysis.md` (if exists) and assembles shared context using Part 1 of [references/subagent-prompt-template.md](references/subagent-prompt-template.md) — inlining the resolved style preset (from `--style` flag, EXTEND.md `style` setting, or default `storytelling`), content background, merged glossary, and comprehension challenges
- Save as `02-prompt.md` in the output directory (shared context only, no task instructions) - Save as `02-prompt.md` in the output directory (shared context only, no task instructions)
5. **Draft translation via subagents** (if Agent tool available): 5. **Draft translation via subagents** (if Agent tool available):
- Spawn one subagent **per chunk**, all in parallel (Part 2 of the template) - Spawn one subagent **per chunk**, all in parallel (Part 2 of the template)
@@ -215,7 +215,7 @@ Before translating chunks:
- **Image-language awareness**: Preserve image references exactly during translation, but after the translation is complete, review referenced images and check whether their likely main text language still matches the translated article language - **Image-language awareness**: Preserve image references exactly during translation, but after the translation is complete, review referenced images and check whether their likely main text language still matches the translated article language
- **Frontmatter transformation**: If the source has YAML frontmatter, preserve it in the translation with these changes: (1) Rename metadata fields that describe the *source* article — `url``sourceUrl`, `title``sourceTitle`, `description``sourceDescription`, `author``sourceAuthor`, `date``sourceDate`, and any similar origin-metadata fields — by adding a `source` prefix (camelCase). (2) Translate the values of text fields (title, description, etc.) and add them as new top-level fields. (3) Keep other fields (tags, categories, custom fields) as-is, translating their values where appropriate - **Frontmatter transformation**: If the source has YAML frontmatter, preserve it in the translation with these changes: (1) Rename metadata fields that describe the *source* article — `url``sourceUrl`, `title``sourceTitle`, `description``sourceDescription`, `author``sourceAuthor`, `date``sourceDate`, and any similar origin-metadata fields — by adding a `source` prefix (camelCase). (2) Translate the values of text fields (title, description, etc.) and add them as new top-level fields. (3) Keep other fields (tags, categories, custom fields) as-is, translating their values where appropriate
- **Respect original**: Maintain original meaning and intent; do not add, remove, or editorialize — but sentence structure and imagery may be adapted freely to serve the meaning - **Respect original**: Maintain original meaning and intent; do not add, remove, or editorialize — but sentence structure and imagery may be adapted freely to serve the meaning
- **Translator's notes**: For terms, concepts, or cultural references that target readers may not understand — due to jargon, cultural gaps, or domain-specific knowledge — add a concise explanatory note in parentheses immediately after the term. The note should explain *what it means* in plain language, not just provide the English original. Format: `译文(English original,通俗解释)`. Calibrate annotation depth to the target audience: general readers need more notes than technical readers. Only add notes where genuinely needed; do not over-annotate obvious terms. - **Translator's notes**: For terms, concepts, or cultural references that target readers may not understand — due to jargon, cultural gaps, or domain-specific knowledge — add a concise explanatory note in parentheses immediately after the term. The note should explain *what it means* in plain language, not just provide the English original. Format: `译文(English original,通俗解释)`. Calibrate annotation depth to the target audience: general readers need more notes than technical readers. For short texts (< 5 sentences), further reduce annotations — only annotate non-common terms that the target audience is unlikely to know; skip terms that are widely recognized or self-explanatory in context. Only add notes where genuinely needed; do not over-annotate obvious terms.
#### Quick Mode #### Quick Mode
@@ -224,7 +224,7 @@ Translate directly → save to `translation.md`. No analysis file, but still app
#### Normal Mode #### Normal Mode
1. **Analyze**`01-analysis.md` (domain, tone, audience, terminology, reader comprehension challenges, figurative language & metaphor mapping) 1. **Analyze**`01-analysis.md` (domain, tone, audience, terminology, reader comprehension challenges, figurative language & metaphor mapping)
2. **Assemble prompt**`02-prompt.md` (translation instructions with inlined context) 2. **Assemble prompt**`02-prompt.md` (translation instructions with inlined style preset, content background, glossary, and comprehension challenges)
3. **Translate** (following `02-prompt.md`) → `translation.md` 3. **Translate** (following `02-prompt.md`) → `translation.md`
After completion, prompt user: "Translation saved. To further review and polish, reply **继续润色** or **refine**." After completion, prompt user: "Translation saved. To further review and polish, reply **继续润色** or **refine**."
@@ -121,7 +121,7 @@ Implicit assumptions: [unstated premises]
## Step 2: Assemble Translation Prompt ## Step 2: Assemble Translation Prompt
Main agent reads `01-analysis.md` and assembles a complete translation prompt using [references/subagent-prompt-template.md](subagent-prompt-template.md). Inline content background, merged glossary, and comprehension challenges into the prompt. Save to `02-prompt.md`. Main agent reads `01-analysis.md` and assembles a complete translation prompt using [references/subagent-prompt-template.md](subagent-prompt-template.md). Inline the resolved style preset (from `--style` flag, EXTEND.md `style` setting, or default `storytelling`), content background, merged glossary, and comprehension challenges into the prompt. Save to `02-prompt.md`.
This prompt is used by the subagent (chunked) or by the main agent itself (non-chunked). This prompt is used by the subagent (chunked) or by the main agent itself (non-chunked).