mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-17 18:32:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62480531c6 | ||
|
|
be990c8057 | ||
|
|
b7482e750c | ||
|
|
f65d5e84c2 | ||
|
|
da1dab3ec8 | ||
|
|
e0d4948e98 | ||
|
|
cd1abd2b14 | ||
|
|
a0fd033cf0 | ||
|
|
446be4b4d6 | ||
|
|
507a4cb413 | ||
|
|
ee51e9a446 | ||
|
|
6ff1dbb014 | ||
|
|
0dc7b1ca30 | ||
|
|
dd5a6c88cc |
@@ -45,7 +45,13 @@ type ListSkillsResult = {
|
||||
version: string
|
||||
createdAt: number
|
||||
changelog: string
|
||||
parsed?: { clawdis?: { os?: string[]; nix?: { plugin?: boolean; systems?: string[] } } }
|
||||
parsed?: {
|
||||
clawdis?: {
|
||||
os?: string[]
|
||||
nix?: { plugin?: boolean; systems?: string[] }
|
||||
capabilities?: string[]
|
||||
}
|
||||
}
|
||||
} | null
|
||||
}>
|
||||
nextCursor: string | null
|
||||
@@ -205,6 +211,7 @@ export async function listSkillsV1Handler(ctx: ActionCtx, request: Request) {
|
||||
version: item.latestVersion.version,
|
||||
createdAt: item.latestVersion.createdAt,
|
||||
changelog: item.latestVersion.changelog,
|
||||
capabilities: item.latestVersion.parsed?.clawdis?.capabilities ?? [],
|
||||
}
|
||||
: null,
|
||||
metadata: item.latestVersion?.parsed?.clawdis
|
||||
@@ -301,6 +308,7 @@ export async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request)
|
||||
version: result.latestVersion.version,
|
||||
createdAt: result.latestVersion.createdAt,
|
||||
changelog: result.latestVersion.changelog,
|
||||
capabilities: result.latestVersion.parsed?.clawdis?.capabilities ?? [],
|
||||
}
|
||||
: null,
|
||||
metadata: result.latestVersion?.parsed?.clawdis
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
const SKILL_CAPABILITIES = [
|
||||
'shell',
|
||||
'filesystem',
|
||||
'network',
|
||||
'browser',
|
||||
'sessions',
|
||||
'messaging',
|
||||
'scheduling',
|
||||
] as const
|
||||
|
||||
export type SkillCapability = (typeof SKILL_CAPABILITIES)[number]
|
||||
|
||||
const SKILL_CAPABILITY_SET = new Set<string>(SKILL_CAPABILITIES)
|
||||
|
||||
const CAPABILITY_ALIASES: Record<string, SkillCapability> = {
|
||||
// shell
|
||||
bash: 'shell',
|
||||
command: 'shell',
|
||||
commands: 'shell',
|
||||
exec: 'shell',
|
||||
process: 'shell',
|
||||
shell: 'shell',
|
||||
terminal: 'shell',
|
||||
shell_exec: 'shell',
|
||||
|
||||
// filesystem
|
||||
apply_patch: 'filesystem',
|
||||
edit: 'filesystem',
|
||||
file: 'filesystem',
|
||||
files: 'filesystem',
|
||||
filesystem: 'filesystem',
|
||||
fs: 'filesystem',
|
||||
write: 'filesystem',
|
||||
|
||||
// network
|
||||
fetch: 'network',
|
||||
http: 'network',
|
||||
mcp: 'network',
|
||||
network: 'network',
|
||||
web: 'network',
|
||||
'web-fetch': 'network',
|
||||
web_fetch: 'network',
|
||||
webfetch: 'network',
|
||||
web_search: 'network',
|
||||
'network.fetch': 'network',
|
||||
'network.search': 'network',
|
||||
|
||||
// browser
|
||||
browser: 'browser',
|
||||
'computer-use': 'browser',
|
||||
computer_use: 'browser',
|
||||
gui: 'browser',
|
||||
screen: 'browser',
|
||||
ui: 'browser',
|
||||
|
||||
// sessions
|
||||
delegate: 'sessions',
|
||||
orchestration: 'sessions',
|
||||
sessions: 'sessions',
|
||||
sessions_send: 'sessions',
|
||||
sessions_spawn: 'sessions',
|
||||
subagent: 'sessions',
|
||||
subagents: 'sessions',
|
||||
|
||||
// messaging
|
||||
chat: 'messaging',
|
||||
message: 'messaging',
|
||||
messages: 'messaging',
|
||||
messaging: 'messaging',
|
||||
|
||||
// scheduling
|
||||
cron: 'scheduling',
|
||||
schedule: 'scheduling',
|
||||
scheduler: 'scheduling',
|
||||
scheduling: 'scheduling',
|
||||
timer: 'scheduling',
|
||||
}
|
||||
|
||||
function normalizeCapabilityName(input: string): SkillCapability | null {
|
||||
const key = input.trim().toLowerCase()
|
||||
if (!key) return null
|
||||
if (SKILL_CAPABILITY_SET.has(key)) return key as SkillCapability
|
||||
const alias = CAPABILITY_ALIASES[key]
|
||||
if (alias) return alias
|
||||
const firstSegment = key.split(/[._:-]/)[0]
|
||||
if (SKILL_CAPABILITY_SET.has(firstSegment)) return firstSegment as SkillCapability
|
||||
return null
|
||||
}
|
||||
|
||||
function extractCapabilityNames(input: unknown): string[] {
|
||||
if (!input) return []
|
||||
if (typeof input === 'string') return [input]
|
||||
if (Array.isArray(input)) {
|
||||
return input.flatMap((entry) => {
|
||||
if (typeof entry === 'string') return [entry]
|
||||
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return []
|
||||
const obj = entry as Record<string, unknown>
|
||||
const named = [obj.name, obj.type, obj.id, obj.capability].find(
|
||||
(value) => typeof value === 'string',
|
||||
)
|
||||
return typeof named === 'string' ? [named] : []
|
||||
})
|
||||
}
|
||||
if (typeof input === 'object') {
|
||||
return Object.keys(input as Record<string, unknown>)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function normalizeCapabilities(input: unknown): SkillCapability[] {
|
||||
const rawNames = extractCapabilityNames(input)
|
||||
const out = new Set<SkillCapability>()
|
||||
for (const rawName of rawNames) {
|
||||
const normalized = normalizeCapabilityName(rawName)
|
||||
if (normalized) out.add(normalized)
|
||||
}
|
||||
return Array.from(out)
|
||||
}
|
||||
@@ -61,6 +61,57 @@ describe('skills utils', () => {
|
||||
expect(clawdis?.requires?.bins).toEqual(['rg'])
|
||||
})
|
||||
|
||||
it('parses capabilities from clawdis metadata', () => {
|
||||
const frontmatter = parseFrontmatter(
|
||||
`---\nmetadata: {"clawdis":{"capabilities":["shell","network","unknown"]}}\n---\nBody`,
|
||||
)
|
||||
const clawdis = parseClawdisMetadata(frontmatter)
|
||||
expect(clawdis?.capabilities).toEqual(['shell', 'network'])
|
||||
})
|
||||
|
||||
it('normalizes alias capability names and new canonical values', () => {
|
||||
const frontmatter = parseFrontmatter(
|
||||
`---\nmetadata: {"clawdis":{"capabilities":["terminal","web_fetch","subagent","cron","message"]}}\n---\nBody`,
|
||||
)
|
||||
const clawdis = parseClawdisMetadata(frontmatter)
|
||||
expect(clawdis?.capabilities).toEqual([
|
||||
'shell',
|
||||
'network',
|
||||
'sessions',
|
||||
'scheduling',
|
||||
'messaging',
|
||||
])
|
||||
})
|
||||
|
||||
it('accepts object-style capabilities with inline constraints', () => {
|
||||
const frontmatter = parseFrontmatter(`---
|
||||
metadata:
|
||||
clawdis:
|
||||
capabilities:
|
||||
shell:
|
||||
mode: restricted
|
||||
allow: [git, gh]
|
||||
network:
|
||||
web_search: true
|
||||
web_fetch: true
|
||||
---`)
|
||||
const clawdis = parseClawdisMetadata(frontmatter)
|
||||
expect(clawdis?.capabilities).toEqual(['shell', 'network'])
|
||||
})
|
||||
|
||||
it('prefers openclaw capabilities when mixed with legacy clawdis metadata', () => {
|
||||
const frontmatter = parseFrontmatter(`---
|
||||
metadata:
|
||||
clawdis:
|
||||
emoji: "🦞"
|
||||
openclaw:
|
||||
capabilities: [terminal, web_fetch]
|
||||
---`)
|
||||
const clawdis = parseClawdisMetadata(frontmatter)
|
||||
expect(clawdis?.emoji).toBe('🦞')
|
||||
expect(clawdis?.capabilities).toEqual(['shell', 'network'])
|
||||
})
|
||||
|
||||
it('ignores invalid clawdis metadata', () => {
|
||||
const frontmatter = parseFrontmatter(`---\nmetadata: not-json\n---\nBody`)
|
||||
expect(parseClawdisMetadata(frontmatter)).toBeUndefined()
|
||||
|
||||
+10
-2
@@ -9,6 +9,7 @@ import {
|
||||
TEXT_FILE_EXTENSION_SET,
|
||||
} from 'clawhub-schema'
|
||||
import { parse as parseYaml } from 'yaml'
|
||||
import { normalizeCapabilities } from './skillCapabilities'
|
||||
|
||||
export type ParsedSkillFrontmatter = Record<string, unknown>
|
||||
export type { ClawdisSkillMetadata, SkillInstallSpec }
|
||||
@@ -70,13 +71,17 @@ export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) {
|
||||
const clawdbotMeta = metadataRecord?.clawdbot
|
||||
const clawdisMeta = metadataRecord?.clawdis
|
||||
const openclawMeta = metadataRecord?.openclaw
|
||||
const openclawObj =
|
||||
openclawMeta && typeof openclawMeta === 'object' && !Array.isArray(openclawMeta)
|
||||
? (openclawMeta as Record<string, unknown>)
|
||||
: undefined
|
||||
const metadataSource =
|
||||
clawdbotMeta && typeof clawdbotMeta === 'object' && !Array.isArray(clawdbotMeta)
|
||||
? (clawdbotMeta as Record<string, unknown>)
|
||||
: clawdisMeta && typeof clawdisMeta === 'object' && !Array.isArray(clawdisMeta)
|
||||
? (clawdisMeta as Record<string, unknown>)
|
||||
: openclawMeta && typeof openclawMeta === 'object' && !Array.isArray(openclawMeta)
|
||||
? (openclawMeta as Record<string, unknown>)
|
||||
: openclawObj
|
||||
? openclawObj
|
||||
: undefined
|
||||
const clawdisRaw = metadataSource ?? frontmatter.clawdis
|
||||
|
||||
@@ -139,6 +144,9 @@ export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) {
|
||||
if (typeof clawdisObj.author === 'string') metadata.author = clawdisObj.author
|
||||
const links = parseSkillLinks(clawdisObj.links)
|
||||
if (links) metadata.links = links
|
||||
// Prefer canonical openclaw capability declarations when present, even in mixed legacy manifests.
|
||||
const capabilities = normalizeCapabilities(openclawObj?.capabilities ?? clawdisObj.capabilities)
|
||||
if (capabilities.length > 0) metadata.capabilities = capabilities
|
||||
|
||||
return parseArk(ClawdisSkillMetadataSchema, metadata, 'Clawdis metadata')
|
||||
} catch {
|
||||
|
||||
@@ -82,9 +82,63 @@ metadata:
|
||||
| `emoji` | `string` | Display emoji for the skill. |
|
||||
| `homepage` | `string` | URL to the skill's homepage or docs. |
|
||||
| `os` | `string[]` | OS restrictions (e.g. `["macos"]`, `["linux"]`). |
|
||||
| `capabilities` | `string[] \| object \| object[]` | Capability declarations (`shell`, `filesystem`, `network`, `browser`, `sessions`, `messaging`, `scheduling`). |
|
||||
| `install` | `array` | Install specs for dependencies (see below). |
|
||||
| `nix` | `object` | Nix plugin spec (see README). |
|
||||
| `config` | `object` | Clawdbot config spec (see README). |
|
||||
| `cliHelp` | `string` | Optional CLI help text shown in skill details. |
|
||||
| `envVars` | `array` | Structured env var declarations (`name`, `required`, `description`). |
|
||||
| `dependencies` | `array` | Structured dependency declarations (`name`, `type`, optional metadata). |
|
||||
| `author` | `string` | Optional skill author string. |
|
||||
| `links` | `object` | Optional links (`homepage`, `repository`, `documentation`, `changelog`). |
|
||||
|
||||
### Capabilities shape and normalization
|
||||
|
||||
`metadata.openclaw.capabilities` supports flat and 2-layer shapes under the same key.
|
||||
|
||||
Flat list:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
openclaw:
|
||||
capabilities: [shell, network, sessions]
|
||||
```
|
||||
|
||||
2-layer object (constraints as key/value pairs):
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
openclaw:
|
||||
capabilities:
|
||||
shell:
|
||||
mode: restricted
|
||||
allow: [git, gh]
|
||||
network:
|
||||
web_search: true
|
||||
web_fetch: true
|
||||
```
|
||||
|
||||
Array-of-objects is also accepted:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
openclaw:
|
||||
capabilities:
|
||||
- type: network.search
|
||||
constraints:
|
||||
provider: brave
|
||||
- name: shell.exec
|
||||
constraints:
|
||||
mode: restricted
|
||||
```
|
||||
|
||||
Aliases are normalized to canonicals at parse time:
|
||||
|
||||
- `web_fetch`, `web_search`, `webfetch` -> `network`
|
||||
- `terminal`, `bash`, `exec` -> `shell`
|
||||
- `subagent`, `sessions_spawn` -> `sessions`
|
||||
- `message` -> `messaging`
|
||||
- `cron`, `schedule` -> `scheduling`
|
||||
|
||||
### Install specs
|
||||
|
||||
|
||||
@@ -171,6 +171,7 @@ export const ApiV1SkillListResponseSchema = type({
|
||||
version: 'string',
|
||||
createdAt: 'number',
|
||||
changelog: 'string',
|
||||
capabilities: '("shell"|"filesystem"|"network"|"browser"|"sessions"|"messaging"|"scheduling")[]?',
|
||||
}).optional(),
|
||||
}).array(),
|
||||
nextCursor: 'string|null',
|
||||
@@ -190,6 +191,7 @@ export const ApiV1SkillResponseSchema = type({
|
||||
version: 'string',
|
||||
createdAt: 'number',
|
||||
changelog: 'string',
|
||||
capabilities: '("shell"|"filesystem"|"network"|"browser"|"sessions"|"messaging"|"scheduling")[]?',
|
||||
}).or('null'),
|
||||
owner: type({
|
||||
handle: 'string|null',
|
||||
@@ -295,6 +297,11 @@ export const ClawdisRequiresSchema = type({
|
||||
})
|
||||
export type ClawdisRequires = (typeof ClawdisRequiresSchema)[inferred]
|
||||
|
||||
export const SkillCapabilitySchema = type(
|
||||
'"shell"|"filesystem"|"network"|"browser"|"sessions"|"messaging"|"scheduling"',
|
||||
)
|
||||
export type SkillCapability = (typeof SkillCapabilitySchema)[inferred]
|
||||
|
||||
export const EnvVarDeclarationSchema = type({
|
||||
name: 'string',
|
||||
required: 'boolean?',
|
||||
@@ -335,5 +342,6 @@ export const ClawdisSkillMetadataSchema = type({
|
||||
dependencies: DependencyDeclarationSchema.array().optional(),
|
||||
author: 'string?',
|
||||
links: SkillLinksSchema.optional(),
|
||||
capabilities: SkillCapabilitySchema.array().optional(),
|
||||
})
|
||||
export type ClawdisSkillMetadata = (typeof ClawdisSkillMetadataSchema)[inferred]
|
||||
|
||||
Reference in New Issue
Block a user