Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions convex/httpApiV1/skillsV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,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
Expand Down Expand Up @@ -301,6 +302,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
Expand Down
118 changes: 118 additions & 0 deletions convex/lib/skillCapabilities.ts
Original file line number Diff line number Diff line change
@@ -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)
}
38 changes: 38 additions & 0 deletions convex/lib/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,44 @@ 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('ignores invalid clawdis metadata', () => {
const frontmatter = parseFrontmatter(`---\nmetadata: not-json\n---\nBody`)
expect(parseClawdisMetadata(frontmatter)).toBeUndefined()
Expand Down
3 changes: 3 additions & 0 deletions convex/lib/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -139,6 +140,8 @@ export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) {
if (typeof clawdisObj.author === 'string') metadata.author = clawdisObj.author
const links = parseSkillLinks(clawdisObj.links)
if (links) metadata.links = links
const capabilities = normalizeCapabilities(clawdisObj.capabilities)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read openclaw capabilities even when clawdis metadata exists

This normalization only reads clawdisObj.capabilities, and clawdisObj comes from the first metadata alias selected earlier (clawdbot/clawdis before openclaw). In mixed manifests that keep legacy metadata.clawdis fields but add new metadata.openclaw.capabilities (the format documented in this PR), the declared capabilities are silently dropped, so downstream API consumers see [] instead of the author-declared capabilities.

Useful? React with 👍 / 👎.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in b7482e7 (+ coverage in be990c8). Capability parsing now prefers metadata.openclaw.capabilities when present, even when other legacy metadata namespaces exist in the same frontmatter.

if (capabilities.length > 0) metadata.capabilities = capabilities

return parseArk(ClawdisSkillMetadataSchema, metadata, 'Clawdis metadata')
} catch {
Expand Down
18 changes: 13 additions & 5 deletions packages/schema/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,12 @@ export const ApiV1SkillListResponseSchema = type({
stats: 'unknown',
createdAt: 'number',
updatedAt: 'number',
latestVersion: type({
version: 'string',
createdAt: 'number',
changelog: 'string',
}).optional(),
latestVersion: type({
version: 'string',
createdAt: 'number',
changelog: 'string',
capabilities: '("shell"|"filesystem"|"network"|"browser"|"sessions"|"messaging"|"scheduling")[]?',
}).optional(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inconsistent indentation (should be 4 spaces like other fields, not 6)

Suggested change
latestVersion: type({
version: 'string',
createdAt: 'number',
changelog: 'string',
capabilities: '("shell"|"filesystem"|"network"|"browser"|"sessions"|"messaging"|"scheduling")[]?',
}).optional(),
latestVersion: type({
version: 'string',
createdAt: 'number',
changelog: 'string',
capabilities: '("shell"|"filesystem"|"network"|"browser"|"sessions"|"messaging"|"scheduling")[]?',
}).optional(),

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/schema/src/schemas.ts
Line: 170-175

Comment:
inconsistent indentation (should be 4 spaces like other fields, not 6)

```suggestion
    latestVersion: type({
      version: 'string',
      createdAt: 'number',
      changelog: 'string',
      capabilities: '("shell"|"filesystem"|"network"|"browser"|"sessions"|"messaging"|"scheduling")[]?',
    }).optional(),
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6248053. Indentation has been normalized to match surrounding schema fields.

}).array(),
nextCursor: 'string|null',
})
Expand All @@ -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',
Expand Down Expand Up @@ -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?',
Expand Down Expand Up @@ -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]
Loading