mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-02 15:23:19 +08:00
- Updated `claude-cli-tools.ts` to support new model configurations and migration from older versions. - Added `getPredefinedModels` and `getAllPredefinedModels` functions for better model management. - Deprecated `cli-config-manager.ts` in favor of `claude-cli-tools.ts`, maintaining backward compatibility. - Introduced `skill-context-loader.ts` to handle skill context loading based on user prompts and keywords. - Enhanced tool configuration functions to include secondary models and improved migration logic. - Updated index file to register the new skill context loader tool.
214 lines
6.2 KiB
TypeScript
214 lines
6.2 KiB
TypeScript
/**
|
|
* Skill Context Loader Tool
|
|
* Loads SKILL context based on keyword matching in user prompt
|
|
* Used by UserPromptSubmit hooks to inject skill context
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { homedir } from 'os';
|
|
|
|
// Input schema for keyword mode config
|
|
const SkillConfigSchema = z.object({
|
|
skill: z.string(),
|
|
keywords: z.array(z.string())
|
|
});
|
|
|
|
// Main params schema
|
|
const ParamsSchema = z.object({
|
|
// Auto mode flag
|
|
mode: z.literal('auto').optional(),
|
|
// User prompt to match against
|
|
prompt: z.string(),
|
|
// Keyword mode configs (only for keyword mode)
|
|
configs: z.array(SkillConfigSchema).optional()
|
|
});
|
|
|
|
type Params = z.infer<typeof ParamsSchema>;
|
|
|
|
/**
|
|
* Get all available skill names from project and user directories
|
|
*/
|
|
function getAvailableSkills(): Array<{ name: string; folderName: string; location: 'project' | 'user' }> {
|
|
const skills: Array<{ name: string; folderName: string; location: 'project' | 'user' }> = [];
|
|
|
|
// Project skills
|
|
const projectSkillsDir = join(process.cwd(), '.claude', 'skills');
|
|
if (existsSync(projectSkillsDir)) {
|
|
try {
|
|
const entries = readdirSync(projectSkillsDir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory()) {
|
|
const skillMdPath = join(projectSkillsDir, entry.name, 'SKILL.md');
|
|
if (existsSync(skillMdPath)) {
|
|
const name = parseSkillName(skillMdPath) || entry.name;
|
|
skills.push({ name, folderName: entry.name, location: 'project' });
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore errors
|
|
}
|
|
}
|
|
|
|
// User skills
|
|
const userSkillsDir = join(homedir(), '.claude', 'skills');
|
|
if (existsSync(userSkillsDir)) {
|
|
try {
|
|
const entries = readdirSync(userSkillsDir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory()) {
|
|
const skillMdPath = join(userSkillsDir, entry.name, 'SKILL.md');
|
|
if (existsSync(skillMdPath)) {
|
|
const name = parseSkillName(skillMdPath) || entry.name;
|
|
// Skip if already added from project (project takes priority)
|
|
if (!skills.some(s => s.folderName === entry.name)) {
|
|
skills.push({ name, folderName: entry.name, location: 'user' });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore errors
|
|
}
|
|
}
|
|
|
|
return skills;
|
|
}
|
|
|
|
/**
|
|
* Parse skill name from SKILL.md frontmatter
|
|
*/
|
|
function parseSkillName(skillMdPath: string): string | null {
|
|
try {
|
|
const content = readFileSync(skillMdPath, 'utf8');
|
|
if (content.startsWith('---')) {
|
|
const endIndex = content.indexOf('---', 3);
|
|
if (endIndex > 0) {
|
|
const frontmatter = content.substring(3, endIndex);
|
|
const nameMatch = frontmatter.match(/^name:\s*["']?([^"'\n]+)["']?/m);
|
|
if (nameMatch) {
|
|
return nameMatch[1].trim();
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore errors
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Match prompt against keywords (case-insensitive)
|
|
*/
|
|
function matchKeywords(prompt: string, keywords: string[]): string | null {
|
|
const lowerPrompt = prompt.toLowerCase();
|
|
for (const keyword of keywords) {
|
|
if (keyword && lowerPrompt.includes(keyword.toLowerCase())) {
|
|
return keyword;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Format skill invocation instruction for hook output
|
|
* Returns a prompt to invoke the skill, not the full content
|
|
*/
|
|
function formatSkillInvocation(skillName: string, matchedKeyword?: string): string {
|
|
return `Use /${skillName} skill to handle this request.`;
|
|
}
|
|
|
|
/**
|
|
* Tool schema definition
|
|
*/
|
|
export const schema: ToolSchema = {
|
|
name: 'skill_context_loader',
|
|
description: 'Match keywords in user prompt and return skill invocation instruction. Returns "Use /skill-name skill" when keywords match.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
mode: {
|
|
type: 'string',
|
|
enum: ['auto'],
|
|
description: 'Auto mode: detect skill name in prompt automatically'
|
|
},
|
|
prompt: {
|
|
type: 'string',
|
|
description: 'User prompt to match against keywords'
|
|
},
|
|
configs: {
|
|
type: 'array',
|
|
description: 'Keyword mode: array of skill configs with keywords',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
skill: { type: 'string', description: 'Skill folder name to load' },
|
|
keywords: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'Keywords to match in prompt'
|
|
}
|
|
},
|
|
required: ['skill', 'keywords']
|
|
}
|
|
}
|
|
},
|
|
required: ['prompt']
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Tool handler
|
|
*/
|
|
export async function handler(params: Record<string, unknown>): Promise<ToolResult<string>> {
|
|
try {
|
|
const parsed = ParamsSchema.parse(params);
|
|
const { mode, prompt, configs } = parsed;
|
|
|
|
// Auto mode: detect skill name in prompt
|
|
if (mode === 'auto') {
|
|
const skills = getAvailableSkills();
|
|
const lowerPrompt = prompt.toLowerCase();
|
|
|
|
for (const skill of skills) {
|
|
// Check if prompt contains skill name or folder name
|
|
if (lowerPrompt.includes(skill.name.toLowerCase()) ||
|
|
lowerPrompt.includes(skill.folderName.toLowerCase())) {
|
|
return {
|
|
success: true,
|
|
result: formatSkillInvocation(skill.folderName, skill.name)
|
|
};
|
|
}
|
|
}
|
|
// No match - return empty (silent)
|
|
return { success: true, result: '' };
|
|
}
|
|
|
|
// Keyword mode: match against configured keywords
|
|
if (configs && configs.length > 0) {
|
|
for (const config of configs) {
|
|
const matchedKeyword = matchKeywords(prompt, config.keywords);
|
|
if (matchedKeyword) {
|
|
return {
|
|
success: true,
|
|
result: formatSkillInvocation(config.skill, matchedKeyword)
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// No match - return empty (silent)
|
|
return { success: true, result: '' };
|
|
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return {
|
|
success: false,
|
|
error: `skill_context_loader error: ${message}`
|
|
};
|
|
}
|
|
}
|