mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 05:45:51 +00:00
165 lines
4.4 KiB
TypeScript
165 lines
4.4 KiB
TypeScript
import { mkdir, writeFile } from 'node:fs/promises'
|
|
import { existsSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
|
import { clearCommandsCache } from '../../commands.js'
|
|
import type { Instinct } from './instinctParser.js'
|
|
import { normalizeSkillName } from './learningPolicy.js'
|
|
import type { SkillLearningScope } from './types.js'
|
|
|
|
export type AgentGeneratorOptions = {
|
|
cwd?: string
|
|
globalAgentsDir?: string
|
|
outputRoot?: string
|
|
name?: string
|
|
description?: string
|
|
scope?: SkillLearningScope
|
|
}
|
|
|
|
export type LearnedAgentDraft = {
|
|
name: string
|
|
description: string
|
|
scope: SkillLearningScope
|
|
sourceInstinctIds: string[]
|
|
confidence: number
|
|
content: string
|
|
outputPath: string
|
|
}
|
|
|
|
export function generateAgentDraft(
|
|
instincts: Instinct[],
|
|
options?: AgentGeneratorOptions,
|
|
): LearnedAgentDraft {
|
|
if (instincts.length === 0) {
|
|
throw new Error('Cannot generate an agent draft without instincts')
|
|
}
|
|
|
|
const scope = options?.scope ?? instincts[0]?.scope ?? 'project'
|
|
const rawName = options?.name ?? buildAgentName(instincts)
|
|
const name = normalizeSkillName(rawName)
|
|
const confidence = averageConfidence(instincts)
|
|
const description = options?.description ?? buildDescription(instincts)
|
|
const outputPath = getLearnedAgentPath(name, scope, options)
|
|
const content = buildAgentContent({
|
|
name,
|
|
description,
|
|
confidence,
|
|
instincts,
|
|
})
|
|
|
|
return {
|
|
name,
|
|
description,
|
|
scope,
|
|
sourceInstinctIds: instincts.map(instinct => instinct.id),
|
|
confidence: Number(confidence.toFixed(2)),
|
|
content,
|
|
outputPath,
|
|
}
|
|
}
|
|
|
|
export async function writeLearnedAgent(
|
|
draft: LearnedAgentDraft,
|
|
): Promise<string> {
|
|
await mkdir(draft.outputPath, { recursive: true })
|
|
const filePath = join(draft.outputPath, `${draft.name}.md`)
|
|
if (existsSync(filePath)) return filePath
|
|
await writeFile(filePath, draft.content, 'utf8')
|
|
clearCommandsCache()
|
|
return filePath
|
|
}
|
|
|
|
export function getLearnedAgentPath(
|
|
_name: string,
|
|
scope: SkillLearningScope,
|
|
options?: AgentGeneratorOptions,
|
|
): string {
|
|
if (options?.outputRoot) return options.outputRoot
|
|
if (scope === 'project') {
|
|
return join(options?.cwd ?? process.cwd(), '.claude', 'agents')
|
|
}
|
|
return options?.globalAgentsDir ?? join(getClaudeConfigHomeDir(), 'agents')
|
|
}
|
|
|
|
function buildAgentName(instincts: Instinct[]): string {
|
|
const words = extractWords(instincts, 4)
|
|
const name = ['learned', 'agent', ...words].join('-')
|
|
return normalizeSkillName(name) || 'learned-agent'
|
|
}
|
|
|
|
function buildDescription(instincts: Instinct[]): string {
|
|
const trigger = instincts[0]?.trigger ?? 'Run the learned multi-step workflow'
|
|
return trigger.replace(/\s+/g, ' ').slice(0, 120)
|
|
}
|
|
|
|
function buildAgentContent(params: {
|
|
name: string
|
|
description: string
|
|
confidence: number
|
|
instincts: Instinct[]
|
|
}): string {
|
|
const { name, description, confidence, instincts } = params
|
|
return [
|
|
'---',
|
|
`name: ${name}`,
|
|
`description: ${JSON.stringify(description)}`,
|
|
'origin: skill-learning',
|
|
`confidence: ${Number(confidence.toFixed(2))}`,
|
|
`evolved_from: [${instincts.map(instinct => JSON.stringify(instinct.id)).join(', ')}]`,
|
|
'---',
|
|
'',
|
|
`You are the ${name} learned agent.`,
|
|
'',
|
|
'## Triggers',
|
|
'',
|
|
instincts.map(instinct => `- ${instinct.trigger}`).join('\n'),
|
|
'',
|
|
'## Playbook',
|
|
'',
|
|
instincts.map(instinct => `- ${instinct.action}`).join('\n'),
|
|
'',
|
|
'## Evidence',
|
|
'',
|
|
instincts
|
|
.flatMap(instinct => instinct.evidence.map(evidence => `- ${evidence}`))
|
|
.join('\n'),
|
|
'',
|
|
].join('\n')
|
|
}
|
|
|
|
function averageConfidence(instincts: Instinct[]): number {
|
|
return (
|
|
instincts.reduce((sum, instinct) => sum + instinct.confidence, 0) /
|
|
instincts.length
|
|
)
|
|
}
|
|
|
|
function extractWords(instincts: Instinct[], max: number): string[] {
|
|
const stopWords = new Set([
|
|
'when',
|
|
'with',
|
|
'this',
|
|
'that',
|
|
'user',
|
|
'asks',
|
|
'for',
|
|
'the',
|
|
'and',
|
|
'debug',
|
|
'investigate',
|
|
'research',
|
|
])
|
|
const words: string[] = []
|
|
for (const instinct of instincts) {
|
|
for (const token of `${instinct.trigger} ${instinct.action}`
|
|
.toLowerCase()
|
|
.split(/[^a-z0-9]+/)) {
|
|
if (token.length > 2 && !stopWords.has(token) && !words.includes(token)) {
|
|
words.push(token)
|
|
}
|
|
if (words.length >= max) return words
|
|
}
|
|
}
|
|
return words
|
|
}
|