feat(skill): add invocation controls
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
isSkillName,
|
||||
type SkillCandidate,
|
||||
type SkillDefinition,
|
||||
type SkillInvocationPolicy,
|
||||
type SkillLookupOptions,
|
||||
type SkillProvider,
|
||||
type SkillSource,
|
||||
@@ -74,7 +75,7 @@ interface ParsedSkill {
|
||||
name: string
|
||||
description: string
|
||||
whenToUse?: string
|
||||
disableModelInvocation?: boolean
|
||||
invocation?: SkillInvocationPolicy
|
||||
metadata?: Record<string, unknown>
|
||||
content: string
|
||||
}
|
||||
@@ -136,7 +137,7 @@ export class LocalSkillProvider implements SkillProvider {
|
||||
name: parsed.name,
|
||||
description: parsed.description,
|
||||
...parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {},
|
||||
...parsed.disableModelInvocation !== undefined ? { disableModelInvocation: parsed.disableModelInvocation } : {},
|
||||
...parsed.invocation !== undefined ? { invocation: parsed.invocation } : {},
|
||||
source: candidate.source,
|
||||
provider: this.name,
|
||||
resourceBase: { kind: 'directory', path: locator.directory },
|
||||
@@ -184,7 +185,7 @@ async function discoverRoot(root: SkillRoot, ctx: Context): Promise<SkillCandida
|
||||
name: parsed.name,
|
||||
description: parsed.description,
|
||||
...parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {},
|
||||
...parsed.disableModelInvocation !== undefined ? { disableModelInvocation: parsed.disableModelInvocation } : {},
|
||||
...parsed.invocation !== undefined ? { invocation: parsed.invocation } : {},
|
||||
provider: 'local',
|
||||
source: root.source,
|
||||
rank: root.rank,
|
||||
@@ -263,11 +264,18 @@ async function parseSkillFile(path: string, ctx: Context, signal?: AbortSignal,
|
||||
ctx.logger.warn(`skill file ${path} ignored: invalid skill name "${name}"`)
|
||||
return undefined
|
||||
}
|
||||
let invocation
|
||||
try {
|
||||
invocation = parseInvocationPolicy(parsed.data)
|
||||
} catch (error) {
|
||||
ctx.logger.warn(`skill file ${path} ignored: invalid invocation frontmatter: ${errorMessage(error)}`)
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
...optionalString(parsed.data, 'whenToUse'),
|
||||
...optionalBoolean(parsed.data, 'disableModelInvocation'),
|
||||
...invocation === undefined ? {} : { invocation },
|
||||
...optionalMetadata(parsed.data),
|
||||
content: parsed.body.trim(),
|
||||
}
|
||||
@@ -420,9 +428,43 @@ function optionalString(data: Record<string, unknown>, key: string): { [K in typ
|
||||
return typeof value === 'string' && value.length > 0 ? { [key]: value } : {}
|
||||
}
|
||||
|
||||
function optionalBoolean(data: Record<string, unknown>, key: string): { [K in typeof key]?: boolean } {
|
||||
function parseInvocationPolicy(data: Record<string, unknown>): SkillInvocationPolicy | undefined {
|
||||
rejectLegacyInvocationKey(data, 'disableModelInvocation', 'disable-model-invocation')
|
||||
rejectLegacyInvocationKey(data, 'userInvocable', 'user-invocable')
|
||||
const disableModelInvocation = frontmatterBoolean(data, 'disable-model-invocation')
|
||||
const userInvocable = frontmatterBoolean(data, 'user-invocable')
|
||||
if (disableModelInvocation === undefined && userInvocable === undefined) return undefined
|
||||
return {
|
||||
...disableModelInvocation === undefined ? {} : { disableModelInvocation },
|
||||
...userInvocable === undefined ? {} : { userInvocable },
|
||||
}
|
||||
}
|
||||
|
||||
function rejectLegacyInvocationKey(data: Record<string, unknown>, legacy: string, canonical: string): void {
|
||||
if (Object.hasOwn(data, legacy)) {
|
||||
throw new Error(`frontmatter field "${legacy}" is unsupported; use "${canonical}"`)
|
||||
}
|
||||
}
|
||||
|
||||
function frontmatterBoolean(data: Record<string, unknown>, key: string): boolean | undefined {
|
||||
if (!Object.hasOwn(data, key)) return undefined
|
||||
const value = data[key]
|
||||
return typeof value === 'boolean' ? { [key]: value } : {}
|
||||
if (typeof value === 'boolean') return value
|
||||
if (value === 1 || value === '1') return true
|
||||
if (value === 0 || value === '0') return false
|
||||
if (typeof value === 'string') {
|
||||
switch (value.toLowerCase()) {
|
||||
case 'true':
|
||||
case 'yes':
|
||||
case 'on':
|
||||
return true
|
||||
case 'false':
|
||||
case 'no':
|
||||
case 'off':
|
||||
return false
|
||||
}
|
||||
}
|
||||
throw new TypeError(`frontmatter field "${key}" must be a boolean`)
|
||||
}
|
||||
|
||||
function optionalMetadata(data: Record<string, unknown>): { metadata?: Record<string, unknown> } {
|
||||
|
||||
Reference in New Issue
Block a user