mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
feat: 添加 skill learning 技能学习闭环系统
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from 'src/Tool.js'
|
||||
import { buildTool } from 'src/Tool.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import {
|
||||
DISCOVER_SKILLS_TOOL_NAME,
|
||||
DESCRIPTION,
|
||||
DISCOVER_SKILLS_PROMPT,
|
||||
} from './prompt.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
description: z
|
||||
.string()
|
||||
.describe(
|
||||
'Description of what you want to do. Be specific — e.g. "deploy a Next.js app to Cloudflare Workers" rather than just "deploy".',
|
||||
),
|
||||
limit: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Maximum number of results to return (default: 5)'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type DiscoverInput = z.infer<InputSchema>
|
||||
|
||||
type DiscoverOutput = {
|
||||
results: Array<{ name: string; description: string; score: number }>
|
||||
count: number
|
||||
}
|
||||
|
||||
export const DiscoverSkillsTool = buildTool({
|
||||
name: DISCOVER_SKILLS_TOOL_NAME,
|
||||
searchHint: 'find search discover skills commands tools capabilities',
|
||||
maxResultSizeChars: 10_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return DESCRIPTION
|
||||
},
|
||||
async prompt() {
|
||||
return DISCOVER_SKILLS_PROMPT
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return 'Discover Skills'
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<DiscoverInput>) {
|
||||
return `Searching skills: ${input.description?.slice(0, 80) ?? '...'}`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: DiscoverOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
if (content.count === 0) {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: 'No matching skills found for that description.',
|
||||
}
|
||||
}
|
||||
const lines = content.results.map(
|
||||
(r, i) =>
|
||||
`${i + 1}. **${r.name}** (score: ${r.score.toFixed(2)})\n ${r.description}`,
|
||||
)
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: `Found ${content.count} relevant skill(s):\n\n${lines.join('\n\n')}`,
|
||||
}
|
||||
},
|
||||
|
||||
async call(input: DiscoverInput, context) {
|
||||
const { getSkillIndex, searchSkills } = await import(
|
||||
'src/services/skillSearch/localSearch.js'
|
||||
)
|
||||
const { getCwd } = await import('src/utils/cwd.js')
|
||||
const cwd = getCwd()
|
||||
|
||||
const index = await getSkillIndex(cwd)
|
||||
const results = searchSkills(input.description, index, input.limit ?? 5)
|
||||
|
||||
return {
|
||||
data: {
|
||||
results: results.map(r => ({
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
score: r.score,
|
||||
})),
|
||||
count: results.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { DISCOVER_SKILLS_TOOL_NAME } from '../prompt.js'
|
||||
|
||||
describe('DiscoverSkillsTool', () => {
|
||||
test('DISCOVER_SKILLS_TOOL_NAME is not empty', () => {
|
||||
expect(DISCOVER_SKILLS_TOOL_NAME).toBe('DiscoverSkills')
|
||||
expect(DISCOVER_SKILLS_TOOL_NAME.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('tool exports are functions', async () => {
|
||||
const { DiscoverSkillsTool } = await import('../DiscoverSkillsTool.js')
|
||||
expect(DiscoverSkillsTool).toBeDefined()
|
||||
expect(DiscoverSkillsTool.name).toBe('DiscoverSkills')
|
||||
expect(typeof DiscoverSkillsTool.call).toBe('function')
|
||||
})
|
||||
|
||||
test('tool has correct metadata', async () => {
|
||||
const { DiscoverSkillsTool } = await import('../DiscoverSkillsTool.js')
|
||||
expect(await DiscoverSkillsTool.description()).toContain('skill')
|
||||
expect(DiscoverSkillsTool.userFacingName()).toBe('Discover Skills')
|
||||
expect(DiscoverSkillsTool.isReadOnly()).toBe(true)
|
||||
expect(DiscoverSkillsTool.isConcurrencySafe()).toBe(true)
|
||||
})
|
||||
|
||||
test('renderToolUseMessage formats input', async () => {
|
||||
const { DiscoverSkillsTool } = await import('../DiscoverSkillsTool.js')
|
||||
const msg = DiscoverSkillsTool.renderToolUseMessage({
|
||||
description: 'deploy to cloudflare',
|
||||
})
|
||||
expect(msg).toContain('deploy to cloudflare')
|
||||
})
|
||||
|
||||
test('mapToolResultToToolResultBlockParam formats empty results', async () => {
|
||||
const { DiscoverSkillsTool } = await import('../DiscoverSkillsTool.js')
|
||||
const result = DiscoverSkillsTool.mapToolResultToToolResultBlockParam(
|
||||
{ results: [], count: 0 },
|
||||
'test-id',
|
||||
)
|
||||
expect(result.content).toContain('No matching skills')
|
||||
})
|
||||
|
||||
test('mapToolResultToToolResultBlockParam formats results', async () => {
|
||||
const { DiscoverSkillsTool } = await import('../DiscoverSkillsTool.js')
|
||||
const result = DiscoverSkillsTool.mapToolResultToToolResultBlockParam(
|
||||
{
|
||||
results: [{ name: 'test-skill', description: 'A test skill', score: 0.85 }],
|
||||
count: 1,
|
||||
},
|
||||
'test-id',
|
||||
)
|
||||
expect(result.content).toContain('test-skill')
|
||||
expect(result.content).toContain('0.85')
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,13 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const DISCOVER_SKILLS_TOOL_NAME: string = '';
|
||||
export const DISCOVER_SKILLS_TOOL_NAME = 'DiscoverSkills'
|
||||
|
||||
export const DESCRIPTION =
|
||||
'Search for relevant skills by describing what you want to do'
|
||||
|
||||
export const DISCOVER_SKILLS_PROMPT = `Search for skills relevant to a task description. Returns matching skills ranked by relevance.
|
||||
|
||||
Use this when:
|
||||
- The auto-surfaced skills don't cover your current task
|
||||
- You're pivoting to a different kind of work mid-conversation
|
||||
- You want to find specialized skills for an unusual workflow
|
||||
|
||||
The search uses TF-IDF keyword matching against all registered skills (bundled, user-defined, and MCP-provided). Results include skill name, description, and relevance score.`
|
||||
|
||||
Reference in New Issue
Block a user