feat: prebundle and auto-enable document, self-improving, search skills (#413)

This commit is contained in:
Felix
2026-03-11 18:40:46 +08:00
committed by GitHub
Unverified
parent ce7e890509
commit baa551b30c
12 changed files with 520 additions and 22 deletions

View File

@@ -109,6 +109,12 @@ AIタスクを自動的に実行するようスケジュール設定できます
### 🧩 拡張可能なスキルシステム
事前構築されたスキルでAIエージェントを拡張できます。統合スキルパネルからスキルの閲覧、インストール、管理が可能です。パッケージマネージャーは不要です。
ClawX はドキュメント処理スキル(`pdf``xlsx``docx``pptx`)もフル内容で同梱し、起動時に `~/.openclaw/skills` へ自動配備し、初回インストール時に既定で有効化します。追加の同梱スキル(`find-skills``self-improving-agent``tavily-search``brave-web-search``bocha-skill`)も既定で有効化されますが、必要な API キーが未設定の場合は OpenClaw が実行時に設定エラーを表示します。
主な検索スキルで必要な環境変数:
- `BRAVE_SEARCH_API_KEY`: `brave-web-search`
- `TAVILY_API_KEY`: `tavily-search` 用(上流ランタイムで OAuth 対応の場合あり)
- `BOCHA_API_KEY`: `bocha-skill`
### 🔐 セキュアなプロバイダー統合
複数のAIプロバイダーOpenAI、Anthropicなどに接続でき、資格情報はシステムのネイティブキーチェーンに安全に保存されます。OpenAI は API キーとブラウザ OAuthCodex サブスクリプション)の両方に対応しています。

View File

@@ -109,6 +109,13 @@ Schedule AI tasks to run automatically. Define triggers, set intervals, and let
### 🧩 Extensible Skill System
Extend your AI agents with pre-built skills. Browse, install, and manage skills through the integrated skill panel—no package managers required.
ClawX also pre-bundles full document-processing skills (`pdf`, `xlsx`, `docx`, `pptx`), deploys them automatically to `~/.openclaw/skills` on startup, and enables them by default on first install. Additional bundled skills (`find-skills`, `self-improving-agent`, `tavily-search`, `brave-web-search`, `bocha-skill`) are also enabled by default; if required API keys are missing, OpenClaw will surface configuration errors in runtime.
Environment variables for bundled search skills:
- `BRAVE_SEARCH_API_KEY` for `brave-web-search`
- `TAVILY_API_KEY` for `tavily-search` (OAuth may also be supported by upstream skill runtime)
- `BOCHA_API_KEY` for `bocha-skill`
- `find-skills` and `self-improving-agent` do not require API keys
### 🔐 Secure Provider Integration
Connect to multiple AI providers (OpenAI, Anthropic, and more) with credentials stored securely in your system's native keychain. OpenAI supports both API key and browser OAuth (Codex subscription) sign-in.

View File

@@ -110,6 +110,12 @@ ClawX 直接基于官方 **OpenClaw** 核心构建。无需单独安装,我们
### 🧩 可扩展技能系统
通过预构建的技能扩展 AI 智能体的能力。在集成的技能面板中浏览、安装和管理技能——无需包管理器。
ClawX 还会内置预装完整的文档处理技能(`pdf``xlsx``docx``pptx`),在启动时自动部署到 `~/.openclaw/skills`,并在首次安装时默认启用。额外预装技能(`find-skills``self-improving-agent``tavily-search``brave-web-search``bocha-skill`)也会默认启用;若缺少必需的 API KeyOpenClaw 会在运行时给出配置错误提示。
重点搜索技能所需环境变量:
- `BRAVE_SEARCH_API_KEY`:用于 `brave-web-search`
- `TAVILY_API_KEY`:用于 `tavily-search`(上游运行时也可能支持 OAuth
- `BOCHA_API_KEY`:用于 `bocha-skill`
### 🔐 安全的供应商集成
连接多个 AI 供应商OpenAI、Anthropic 等凭证安全存储在系统原生密钥链中。OpenAI 同时支持 API Key 与浏览器 OAuthCodex 订阅)登录。

View File

@@ -26,6 +26,9 @@ extraResources:
# because electron-builder respects .gitignore which excludes node_modules/)
- from: build/openclaw/
to: openclaw/
# Pre-bundled third-party skills (full directories, not only SKILL.md)
- from: build/preinstalled-skills/
to: resources/preinstalled-skills/
# NOTE: OpenClaw plugin mirrors (dingtalk, etc.) are bundled by the
# afterPack hook (after-pack.cjs) directly from node_modules, so they
# don't need an extraResources entry here.

View File

@@ -77,8 +77,8 @@ export async function handleSkillRoutes(
if (url.pathname === '/api/clawhub/open-readme' && req.method === 'POST') {
try {
const body = await parseJsonBody<{ slug: string }>(req);
await ctx.clawHubService.openSkillReadme(body.slug);
const body = await parseJsonBody<{ slug?: string; skillKey?: string }>(req);
await ctx.clawHubService.openSkillReadme(body.skillKey || body.slug || '', body.slug);
sendJson(res, 200, { success: true });
} catch (error) {
sendJson(res, 500, { success: false, error: String(error) });

View File

@@ -67,6 +67,55 @@ export class ClawHubService {
return line.replace(this.ansiRegex, '').trim();
}
private extractFrontmatterName(skillManifestPath: string): string | null {
try {
const raw = fs.readFileSync(skillManifestPath, 'utf8');
// Match the first frontmatter block and read `name: ...`
const frontmatterMatch = raw.match(/^---\s*\n([\s\S]*?)\n---/);
if (!frontmatterMatch) return null;
const body = frontmatterMatch[1];
const nameMatch = body.match(/^\s*name\s*:\s*["']?([^"'\n]+)["']?\s*$/m);
if (!nameMatch) return null;
const name = nameMatch[1].trim();
return name || null;
} catch {
return null;
}
}
private resolveSkillDirByManifestName(candidates: string[]): string | null {
const skillsRoot = path.join(this.workDir, 'skills');
if (!fs.existsSync(skillsRoot)) return null;
const wanted = new Set(
candidates
.map((v) => v.trim().toLowerCase())
.filter((v) => v.length > 0),
);
if (wanted.size === 0) return null;
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
} catch {
return null;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillDir = path.join(skillsRoot, entry.name);
const skillManifestPath = path.join(skillDir, 'SKILL.md');
if (!fs.existsSync(skillManifestPath)) continue;
const frontmatterName = this.extractFrontmatterName(skillManifestPath);
if (!frontmatterName) continue;
if (wanted.has(frontmatterName.toLowerCase())) {
return skillDir;
}
}
return null;
}
/**
* Run a ClawHub CLI command
*/
@@ -318,24 +367,33 @@ export class ClawHubService {
/**
* Open skill README/manual in default editor
*/
async openSkillReadme(slug: string): Promise<boolean> {
const skillDir = path.join(this.workDir, 'skills', slug);
async openSkillReadme(skillKeyOrSlug: string, fallbackSlug?: string): Promise<boolean> {
const candidates = [skillKeyOrSlug, fallbackSlug]
.filter((v): v is string => typeof v === 'string' && v.trim().length > 0)
.map(v => v.trim());
const uniqueCandidates = [...new Set(candidates)];
const directSkillDir = uniqueCandidates
.map((id) => path.join(this.workDir, 'skills', id))
.find((dir) => fs.existsSync(dir));
const skillDir = directSkillDir || this.resolveSkillDirByManifestName(uniqueCandidates);
// Try to find documentation file
const possibleFiles = ['SKILL.md', 'README.md', 'skill.md', 'readme.md'];
let targetFile = '';
for (const file of possibleFiles) {
const filePath = path.join(skillDir, file);
if (fs.existsSync(filePath)) {
targetFile = filePath;
break;
if (skillDir) {
for (const file of possibleFiles) {
const filePath = path.join(skillDir, file);
if (fs.existsSync(filePath)) {
targetFile = filePath;
break;
}
}
}
if (!targetFile) {
// If no md file, just open the directory
if (fs.existsSync(skillDir)) {
if (skillDir) {
targetFile = skillDir;
} else {
throw new Error('Skill directory not found');

View File

@@ -21,7 +21,7 @@ import { autoInstallCliIfNeeded, generateCompletionCache, installCompletionToPro
import { isQuitting, setQuitting } from './app-state';
import { applyProxySettings } from './proxy';
import { getSetting } from '../utils/store';
import { ensureBuiltinSkillsInstalled } from '../utils/skill-config';
import { ensureBuiltinSkillsInstalled, ensurePreinstalledSkillsInstalled } from '../utils/skill-config';
import { startHostApiServer } from '../api/server';
import { HostEventBus } from '../api/event-bus';
import { deviceOAuthManager } from '../utils/device-oauth';
@@ -236,6 +236,13 @@ async function initialize(): Promise<void> {
logger.warn('Failed to install built-in skills:', error);
});
// Pre-deploy bundled third-party skills from resources/preinstalled-skills.
// This installs full skill directories (not only SKILL.md) in an idempotent,
// non-destructive way and never blocks startup.
void ensurePreinstalledSkillsInstalled().catch((error) => {
logger.warn('Failed to install preinstalled skills:', error);
});
// Bridge gateway and host-side events before any auto-start logic runs, so
// renderer subscribers observe the full startup lifecycle.
gatewayManager.on('status', (status: { state: string }) => {

View File

@@ -10,7 +10,7 @@ import { existsSync } from 'fs';
import { constants } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { getOpenClawDir } from './paths';
import { getOpenClawDir, getResourcesDir } from './paths';
import { logger } from './logger';
const OPENCLAW_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json');
@@ -29,6 +29,32 @@ interface OpenClawConfig {
[key: string]: unknown;
}
interface PreinstalledSkillSpec {
slug: string;
version?: string;
autoEnable?: boolean;
}
interface PreinstalledManifest {
skills?: PreinstalledSkillSpec[];
}
interface PreinstalledLockEntry {
slug: string;
version?: string;
}
interface PreinstalledLockFile {
skills?: PreinstalledLockEntry[];
}
interface PreinstalledMarker {
source: 'clawx-preinstalled';
slug: string;
version: string;
installedAt: string;
}
async function fileExists(p: string): Promise<boolean> {
try { await access(p, constants.F_OK); return true; } catch { return false; }
}
@@ -57,6 +83,25 @@ async function writeConfig(config: OpenClawConfig): Promise<void> {
await writeFile(OPENCLAW_CONFIG_PATH, json, 'utf-8');
}
async function setSkillsEnabled(skillKeys: string[], enabled: boolean): Promise<void> {
if (skillKeys.length === 0) {
return;
}
const config = await readConfig();
if (!config.skills) {
config.skills = {};
}
if (!config.skills.entries) {
config.skills.entries = {};
}
for (const skillKey of skillKeys) {
const entry = config.skills.entries[skillKey] || {};
entry.enabled = enabled;
config.skills.entries[skillKey] = entry;
}
await writeConfig(config);
}
/**
* Get skill config
*/
@@ -177,3 +222,163 @@ export async function ensureBuiltinSkillsInstalled(): Promise<void> {
}
}
}
const PREINSTALLED_MANIFEST_NAME = 'preinstalled-manifest.json';
const PREINSTALLED_MARKER_NAME = '.clawx-preinstalled.json';
async function readPreinstalledManifest(): Promise<PreinstalledSkillSpec[]> {
const candidates = [
join(getResourcesDir(), 'skills', PREINSTALLED_MANIFEST_NAME),
join(process.cwd(), 'resources', 'skills', PREINSTALLED_MANIFEST_NAME),
];
const manifestPath = candidates.find((p) => existsSync(p));
if (!manifestPath) {
return [];
}
try {
const raw = await readFile(manifestPath, 'utf-8');
const parsed = JSON.parse(raw) as PreinstalledManifest;
if (!Array.isArray(parsed.skills)) {
return [];
}
return parsed.skills.filter((s): s is PreinstalledSkillSpec => Boolean(s?.slug));
} catch (error) {
logger.warn('Failed to read preinstalled-skills manifest:', error);
return [];
}
}
function resolvePreinstalledSkillsSourceRoot(): string | null {
const candidates = [
join(getResourcesDir(), 'preinstalled-skills'),
join(process.cwd(), 'build', 'preinstalled-skills'),
join(__dirname, '../../build/preinstalled-skills'),
];
const root = candidates.find((dir) => existsSync(dir));
return root || null;
}
async function readPreinstalledLockVersions(sourceRoot: string): Promise<Map<string, string>> {
const lockPath = join(sourceRoot, '.preinstalled-lock.json');
if (!existsSync(lockPath)) {
return new Map();
}
try {
const raw = await readFile(lockPath, 'utf-8');
const parsed = JSON.parse(raw) as PreinstalledLockFile;
const versions = new Map<string, string>();
for (const entry of parsed.skills || []) {
const slug = entry.slug?.trim();
const version = entry.version?.trim();
if (slug && version) {
versions.set(slug, version);
}
}
return versions;
} catch (error) {
logger.warn('Failed to read preinstalled-skills lock file:', error);
return new Map();
}
}
async function tryReadMarker(markerPath: string): Promise<PreinstalledMarker | null> {
if (!existsSync(markerPath)) {
return null;
}
try {
const raw = await readFile(markerPath, 'utf-8');
const parsed = JSON.parse(raw) as PreinstalledMarker;
if (!parsed?.slug || !parsed?.version) {
return null;
}
return parsed;
} catch {
return null;
}
}
/**
* Ensure third-party preinstalled skills (bundled in app resources) are
* deployed to ~/.openclaw/skills/<slug>/ as full directories.
*
* Policy:
* - If skill is missing locally, install it.
* - If local skill exists without our marker, treat as user-managed and never overwrite.
* - If marker exists with same version, skip.
* - If marker exists with a different version, skip by default to avoid overwriting edits.
*/
export async function ensurePreinstalledSkillsInstalled(): Promise<void> {
const skills = await readPreinstalledManifest();
if (skills.length === 0) {
return;
}
const sourceRoot = resolvePreinstalledSkillsSourceRoot();
if (!sourceRoot) {
logger.warn('Preinstalled skills source root not found; skipping preinstall.');
return;
}
const lockVersions = await readPreinstalledLockVersions(sourceRoot);
const targetRoot = join(homedir(), '.openclaw', 'skills');
await mkdir(targetRoot, { recursive: true });
const toEnable: string[] = [];
for (const spec of skills) {
const sourceDir = join(sourceRoot, spec.slug);
const sourceManifest = join(sourceDir, 'SKILL.md');
if (!existsSync(sourceManifest)) {
logger.warn(`Preinstalled skill source missing SKILL.md, skipping: ${sourceDir}`);
continue;
}
const targetDir = join(targetRoot, spec.slug);
const targetManifest = join(targetDir, 'SKILL.md');
const markerPath = join(targetDir, PREINSTALLED_MARKER_NAME);
const desiredVersion = lockVersions.get(spec.slug)
|| (spec.version || 'unknown').trim()
|| 'unknown';
const marker = await tryReadMarker(markerPath);
if (existsSync(targetManifest)) {
if (!marker) {
logger.info(`Skipping user-managed skill: ${spec.slug}`);
continue;
}
if (marker.version === desiredVersion) {
continue;
}
logger.info(`Skipping preinstalled skill update for ${spec.slug} (local marker version=${marker.version}, desired=${desiredVersion})`);
continue;
}
try {
await mkdir(targetDir, { recursive: true });
await cp(sourceDir, targetDir, { recursive: true, force: true });
const markerPayload: PreinstalledMarker = {
source: 'clawx-preinstalled',
slug: spec.slug,
version: desiredVersion,
installedAt: new Date().toISOString(),
};
await writeFile(markerPath, `${JSON.stringify(markerPayload, null, 2)}\n`, 'utf-8');
if (spec.autoEnable) {
toEnable.push(spec.slug);
}
logger.info(`Installed preinstalled skill: ${spec.slug} -> ${targetDir}`);
} catch (error) {
logger.warn(`Failed to install preinstalled skill ${spec.slug}:`, error);
}
}
if (toEnable.length > 0) {
try {
await setSkillsEnabled(toEnable, true);
} catch (error) {
logger.warn('Failed to auto-enable preinstalled skills:', error);
}
}
}

View File

@@ -24,9 +24,10 @@
"scripts": {
"init": "pnpm install && pnpm run uv:download",
"dev": "vite",
"build": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder",
"build": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs && electron-builder",
"build:vite": "vite build",
"bundle:openclaw-plugins": "zx scripts/bundle-openclaw-plugins.mjs",
"bundle:preinstalled-skills": "zx scripts/bundle-preinstalled-skills.mjs",
"lint": "eslint . --fix",
"typecheck": "tsc --noEmit",
"test": "vitest run",
@@ -36,11 +37,11 @@
"uv:download:linux": "zx scripts/download-bundled-uv.mjs --platform=linux",
"uv:download:all": "zx scripts/download-bundled-uv.mjs --all",
"icons": "zx scripts/generate-icons.mjs",
"package": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder",
"package:mac": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --mac",
"package:win": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --win",
"package:linux": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --linux",
"release": "pnpm run uv:download && vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --publish always",
"package": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs && electron-builder",
"package:mac": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs && electron-builder --mac",
"package:win": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs && electron-builder --win",
"package:linux": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs && electron-builder --linux",
"release": "pnpm run uv:download && vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs && electron-builder --publish always",
"version:patch": "pnpm version patch",
"version:minor": "pnpm version minor",
"version:major": "pnpm version major",

View File

@@ -0,0 +1,76 @@
{
"skills": [
{
"slug": "pdf",
"repo": "anthropics/skills",
"repoPath": "skills/pdf",
"ref": "main",
"version": "main",
"autoEnable": true
},
{
"slug": "xlsx",
"repo": "anthropics/skills",
"repoPath": "skills/xlsx",
"ref": "main",
"version": "main",
"autoEnable": true
},
{
"slug": "docx",
"repo": "anthropics/skills",
"repoPath": "skills/docx",
"ref": "main",
"version": "main",
"autoEnable": true
},
{
"slug": "pptx",
"repo": "anthropics/skills",
"repoPath": "skills/pptx",
"ref": "main",
"version": "main",
"autoEnable": true
},
{
"slug": "find-skills",
"repo": "vercel-labs/skills",
"repoPath": "skills/find-skills",
"ref": "main",
"version": "main",
"autoEnable": true
},
{
"slug": "self-improving-agent",
"repo": "openclaw/skills",
"repoPath": "skills/pskoett/self-improving-agent",
"ref": "main",
"version": "main",
"autoEnable": true
},
{
"slug": "tavily-search",
"repo": "tavily-ai/skills",
"repoPath": "skills/tavily/search",
"ref": "main",
"version": "main",
"autoEnable": true
},
{
"slug": "brave-web-search",
"repo": "brave/brave-search-skills",
"repoPath": "skills/web-search",
"ref": "main",
"version": "main",
"autoEnable": true
},
{
"slug": "bocha-skill",
"repo": "openclaw/skills",
"repoPath": "skills/ypw757/bocha-skill",
"ref": "main",
"version": "main",
"autoEnable": true
}
]
}

View File

@@ -0,0 +1,118 @@
#!/usr/bin/env zx
import 'zx/globals';
import { readFileSync, existsSync, mkdirSync, rmSync, cpSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const MANIFEST_PATH = join(ROOT, 'resources', 'skills', 'preinstalled-manifest.json');
const OUTPUT_ROOT = join(ROOT, 'build', 'preinstalled-skills');
const TMP_ROOT = join(ROOT, 'build', '.tmp-preinstalled-skills');
function loadManifest() {
if (!existsSync(MANIFEST_PATH)) {
throw new Error(`Missing manifest: ${MANIFEST_PATH}`);
}
const raw = readFileSync(MANIFEST_PATH, 'utf8');
const parsed = JSON.parse(raw);
if (!parsed || !Array.isArray(parsed.skills)) {
throw new Error('Invalid preinstalled-skills manifest format');
}
for (const item of parsed.skills) {
if (!item.slug || !item.repo || !item.repoPath) {
throw new Error(`Invalid manifest entry: ${JSON.stringify(item)}`);
}
}
return parsed.skills;
}
function groupByRepoRef(entries) {
const grouped = new Map();
for (const entry of entries) {
const ref = entry.ref || 'main';
const key = `${entry.repo}#${ref}`;
if (!grouped.has(key)) grouped.set(key, { repo: entry.repo, ref, entries: [] });
grouped.get(key).entries.push(entry);
}
return [...grouped.values()];
}
function createRepoDirName(repo, ref) {
return `${repo.replace(/[\\/]/g, '__')}__${ref.replace(/[^a-zA-Z0-9._-]/g, '_')}`;
}
async function fetchSparseRepo(repo, ref, paths, checkoutDir) {
const remote = `https://github.com/${repo}.git`;
mkdirSync(checkoutDir, { recursive: true });
await $`git init ${checkoutDir}`;
await $`git -C ${checkoutDir} remote add origin ${remote}`;
await $`git -C ${checkoutDir} sparse-checkout init --cone`;
await $`git -C ${checkoutDir} sparse-checkout set ${paths}`;
await $`git -C ${checkoutDir} fetch --depth 1 origin ${ref}`;
await $`git -C ${checkoutDir} checkout FETCH_HEAD`;
const commit = (await $`git -C ${checkoutDir} rev-parse HEAD`).stdout.trim();
return commit;
}
echo`Bundling preinstalled skills...`;
const manifestSkills = loadManifest();
rmSync(OUTPUT_ROOT, { recursive: true, force: true });
mkdirSync(OUTPUT_ROOT, { recursive: true });
rmSync(TMP_ROOT, { recursive: true, force: true });
mkdirSync(TMP_ROOT, { recursive: true });
const lock = {
generatedAt: new Date().toISOString(),
skills: [],
};
const groups = groupByRepoRef(manifestSkills);
for (const group of groups) {
const repoDir = join(TMP_ROOT, createRepoDirName(group.repo, group.ref));
const sparsePaths = [...new Set(group.entries.map((entry) => entry.repoPath))];
echo`Fetching ${group.repo} @ ${group.ref}`;
const commit = await fetchSparseRepo(group.repo, group.ref, sparsePaths, repoDir);
echo` commit ${commit}`;
for (const entry of group.entries) {
const sourceDir = join(repoDir, entry.repoPath);
const targetDir = join(OUTPUT_ROOT, entry.slug);
if (!existsSync(sourceDir)) {
throw new Error(`Missing source path in repo checkout: ${entry.repoPath}`);
}
rmSync(targetDir, { recursive: true, force: true });
cpSync(sourceDir, targetDir, { recursive: true, dereference: true });
const skillManifest = join(targetDir, 'SKILL.md');
if (!existsSync(skillManifest)) {
throw new Error(`Skill ${entry.slug} is missing SKILL.md after copy`);
}
const requestedVersion = (entry.version || '').trim();
const resolvedVersion = !requestedVersion || requestedVersion === 'main'
? commit
: requestedVersion;
lock.skills.push({
slug: entry.slug,
version: resolvedVersion,
repo: entry.repo,
repoPath: entry.repoPath,
ref: group.ref,
commit,
});
echo` OK ${entry.slug}`;
}
}
writeFileSync(join(OUTPUT_ROOT, '.preinstalled-lock.json'), `${JSON.stringify(lock, null, 2)}\n`, 'utf8');
rmSync(TMP_ROOT, { recursive: true, force: true });
echo`Preinstalled skills ready: ${OUTPUT_ROOT}`;

View File

@@ -82,11 +82,11 @@ function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall }: Sk
};
const handleOpenEditor = async () => {
if (!skill?.slug) return;
if (!skill?.id) return;
try {
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/open-readme', {
method: 'POST',
body: JSON.stringify({ slug: skill.slug }),
body: JSON.stringify({ skillKey: skill.id, slug: skill.slug }),
});
if (result.success) {
toast.success(t('toast.openedEditor'));
@@ -382,8 +382,14 @@ export function Skills() {
// Filter skills
const safeSkills = Array.isArray(skills) ? skills : [];
const filteredSkills = safeSkills.filter((skill) => {
const matchesSearch = skill.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
skill.description.toLowerCase().includes(searchQuery.toLowerCase());
const q = searchQuery.toLowerCase().trim();
const matchesSearch =
q.length === 0 ||
skill.name.toLowerCase().includes(q) ||
skill.description.toLowerCase().includes(q) ||
skill.id.toLowerCase().includes(q) ||
(skill.slug || '').toLowerCase().includes(q) ||
(skill.author || '').toLowerCase().includes(q);
let matchesSource = true;
if (selectedSource === 'built-in') {
@@ -701,6 +707,11 @@ export function Skills() {
) : skill.isBundled ? (
<Puzzle className="h-3 w-3 text-blue-500/70" />
) : null}
{skill.slug && skill.slug !== skill.name ? (
<span className="text-[11px] font-mono px-1.5 py-0.5 rounded border border-black/10 dark:border-white/10 text-muted-foreground">
{skill.slug}
</span>
) : null}
</div>
<p className="text-[13.5px] text-muted-foreground line-clamp-1 pr-6 leading-relaxed">
{skill.description}