backup materials and knowledge-base docs
This commit is contained in:
235
文档润色流和知识库构建流/claude-scholar-upstream/hooks/skill-forced-eval.js
Normal file
235
文档润色流和知识库构建流/claude-scholar-upstream/hooks/skill-forced-eval.js
Normal file
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* UserPromptSubmit Hook: Forced skill activation flow (cross-platform version)
|
||||
*
|
||||
* Event: UserPromptSubmit
|
||||
* Function: Force AI to evaluate available skills and begin implementation after activation
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const common = require('./hook-common');
|
||||
|
||||
// Read stdin input
|
||||
let input = {};
|
||||
try {
|
||||
const stdinData = require('fs').readFileSync(0, 'utf8');
|
||||
if (stdinData.trim()) {
|
||||
input = JSON.parse(stdinData);
|
||||
}
|
||||
} catch {
|
||||
// Use default empty object
|
||||
}
|
||||
|
||||
const userPrompt = input.user_prompt || '';
|
||||
const cwd = input.cwd || process.cwd();
|
||||
|
||||
// Check if it is a slash command (escape)
|
||||
if (userPrompt.startsWith('/')) {
|
||||
// Distinguish commands from paths:
|
||||
// - Commands: /commit, /update-github (no second slash after the first)
|
||||
// - Paths: /Users/xxx, /path/to/file (contains path separators)
|
||||
const rest = userPrompt.substring(1);
|
||||
if (rest.includes('/')) {
|
||||
// This is a path, continue with skill scanning
|
||||
} else {
|
||||
// This is a command, skip skill evaluation
|
||||
console.log(JSON.stringify({ continue: true }));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const homeDir = os.homedir();
|
||||
|
||||
// Dynamically collect skill list
|
||||
function collectSkills() {
|
||||
const skills = [];
|
||||
const skillsDir = path.join(homeDir, '.claude', 'skills');
|
||||
|
||||
// 1. Collect local skills
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory())
|
||||
.map(d => d.name);
|
||||
|
||||
for (const skillName of skillDirs) {
|
||||
skills.push(skillName);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Collect plugin skills
|
||||
const pluginsCache = path.join(homeDir, '.claude', 'plugins', 'cache');
|
||||
|
||||
if (fs.existsSync(pluginsCache)) {
|
||||
const marketplaces = fs.readdirSync(pluginsCache, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory())
|
||||
.map(d => d.name);
|
||||
|
||||
for (const marketplace of marketplaces) {
|
||||
const marketplacePath = path.join(pluginsCache, marketplace);
|
||||
const plugins = fs.readdirSync(marketplacePath, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
|
||||
.map(d => d.name);
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const pluginPath = path.join(marketplacePath, plugin);
|
||||
const versions = fs.readdirSync(pluginPath, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory())
|
||||
.map(d => d.name)
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
if (versions.length > 0) {
|
||||
const latestVersion = versions[0];
|
||||
const skillsDirPath = path.join(pluginPath, latestVersion, 'skills');
|
||||
|
||||
if (fs.existsSync(skillsDirPath)) {
|
||||
const skillDirs = fs.readdirSync(skillsDirPath, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory())
|
||||
.map(d => d.name);
|
||||
|
||||
for (const skillName of skillDirs) {
|
||||
skills.push(`${plugin}:${skillName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
return [...new Set(skills)].sort();
|
||||
}
|
||||
|
||||
// Categorize skills into groups
|
||||
function categorizeSkills(skills) {
|
||||
const categories = {
|
||||
'Research & Writing': /research|paper|writing|citation|review-response|rebuttal|post-acceptance|doc-coauthoring|latex|daily-paper|ml-paper|results-analysis|results-report|brainstorm/,
|
||||
'Development': /coding|git|code-review|bug|architecture|verification|tdd|uv-package|webapp-testing|kaggle|driven-development|development-branch|planning|dispatching|executing|using-superpowers/,
|
||||
'Plugin Dev': /skill-|command-|hook-|mcp-|agent-identifier|plugin-structure/,
|
||||
'Design & UI': /frontend|ui-ux|web-design|canvas|brand|theme|algorithmic-art|slack-gif|figma/,
|
||||
'Documents': /docx|xlsx|pptx|pdf|internal-comms|web-artifacts/,
|
||||
};
|
||||
|
||||
const grouped = {};
|
||||
for (const cat of Object.keys(categories)) {
|
||||
grouped[cat] = [];
|
||||
}
|
||||
grouped['Other'] = [];
|
||||
|
||||
for (const skill of skills) {
|
||||
let matched = false;
|
||||
for (const [cat, regex] of Object.entries(categories)) {
|
||||
if (regex.test(skill)) {
|
||||
grouped[cat].push(skill);
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
grouped['Other'].push(skill);
|
||||
}
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// Keyword-to-skill mapping for pre-matching
|
||||
// Note: \b doesn't work with CJK characters, so we use separate patterns
|
||||
// English keywords use \b for precision; Chinese keywords match without \b
|
||||
const KEYWORD_SKILL_MAP = [
|
||||
{ keywords: /\b(git|github|commit|push|pull|merge|rebase|branch|tag|stash|cherry.?pick|develop|master|main)\b|分支|合并|推送|提交代码|上传.*分支|主分支/i, skills: ['git-workflow'] },
|
||||
{ keywords: /\b(debug|bug|error|broken|failing|traceback|exception)\b|排查|调试|报错|出错|不工作/i, skills: ['bug-detective'] },
|
||||
{ keywords: /\b(tdd|test.?driven)\b|写测试|测试驱动/i, skills: ['superpowers:test-driven-development'] },
|
||||
{ keywords: /\b(code.?review|review code)\b|审查代码|代码审查/i, skills: ['code-review-excellence'] },
|
||||
{ keywords: /\b(paper|manuscript|draft)\b|论文|写作|投稿/i, skills: ['ml-paper-writing'] },
|
||||
{ keywords: /\b(research|idea|brainstorm)\b|研究|构思|文献/i, skills: ['research-ideation'] },
|
||||
{ keywords: /\b(rebuttal|reviewer|response to reviewer)\b|审稿|回复审稿/i, skills: ['review-response'] },
|
||||
{ keywords: /\b(frontend|landing.?page|dashboard)\b|前端|界面|网页设计/i, skills: ['frontend-design'] },
|
||||
{ keywords: /\b(create|write|develop|improve).*skill|skill.*(开发|创建|写|改进)|开发.*skill|写.*skill|改进.*skill/i, skills: ['skill-development'] },
|
||||
{ keywords: /\b(create|write|develop).*hook|hook.*(开发|创建|写)|开发.*hook|写.*hook/i, skills: ['hook-development'] },
|
||||
{ keywords: /\b(create|write|develop).*command|slash.*command|command.*(开发|创建|写)|开发.*command|写.*命令/i, skills: ['command-development'] },
|
||||
{ keywords: /\b(create|write|develop).*agent|agent.*(开发|创建|写)|开发.*agent|写.*agent/i, skills: ['agent-identifier'] },
|
||||
{ keywords: /\b(mcp)\b|mcp.*server|mcp.*集成/i, skills: ['mcp-integration'] },
|
||||
{ keywords: /\b(architecture|factory|registry)\b|架构|设计模式/i, skills: ['architecture-design'] },
|
||||
{ keywords: /\b(uv|pip|package.*manager|venv)\b|包管理|虚拟环境/i, skills: ['uv-package-manager'] },
|
||||
{ keywords: /\b(kaggle|competition)\b|竞赛/i, skills: ['kaggle-learner'] },
|
||||
{ keywords: /\b(citation|reference.*check)\b|引用|引文|参考文献/i, skills: ['citation-verification'] },
|
||||
{ keywords: /\b(latex.*template|overleaf)\b|模板整理/i, skills: ['latex-conference-template-organizer'] },
|
||||
{ keywords: /\b(ablation)\b|实验结果|results.*analysis|统计检验|消融实验|科学绘图|实验统计/i, skills: ['results-analysis'] },
|
||||
{ keywords: /\b(experiment.?report|results.?report|retrospective|wrap.?up)\b|实验报告|实验总结|实验复盘|结果总结/i, skills: ['results-report'] },
|
||||
{ keywords: /\b(poster|presentation|promote)\b|海报|演讲|推广/i, skills: ['post-acceptance'] },
|
||||
{ keywords: /\b(verify|verification)\b|验证/i, skills: ['verification-loop'] },
|
||||
{ keywords: /\b(self.?review)\b|自审|论文检查/i, skills: ['paper-self-review'] },
|
||||
{ keywords: /\b(anti.?ai|humanize)\b|去.*ai.*痕迹|AI写作/i, skills: ['writing-anti-ai'] },
|
||||
{ keywords: /\b(implement|write code|add feature|modify|refactor)\b|写代码|改代码|实现|添加功能|修改|重构/i, skills: ['daily-coding'] },
|
||||
];
|
||||
|
||||
// Pre-match user prompt against keyword map
|
||||
function suggestSkills(prompt) {
|
||||
const suggested = new Set();
|
||||
for (const { keywords, skills } of KEYWORD_SKILL_MAP) {
|
||||
if (keywords.test(prompt)) {
|
||||
for (const s of skills) suggested.add(s);
|
||||
}
|
||||
}
|
||||
return [...suggested];
|
||||
}
|
||||
|
||||
// Generate skill list
|
||||
const SKILL_LIST = collectSkills();
|
||||
const SKILL_GROUPS = categorizeSkills(SKILL_LIST);
|
||||
const suggestedSkills = suggestSkills(userPrompt);
|
||||
const binding = common.getProjectMemoryBinding(cwd);
|
||||
const isResearchPrompt = common.promptLooksResearchRelated(userPrompt);
|
||||
|
||||
if (binding.bound && isResearchPrompt) {
|
||||
if (SKILL_LIST.includes('obsidian-project-kb-core')) {
|
||||
suggestedSkills.push('obsidian-project-kb-core');
|
||||
}
|
||||
if (/\b(zotero|collection|doi|arxiv|citation)\b|zotero|文献|参考文献|collection/i.test(userPrompt) &&
|
||||
SKILL_LIST.includes('zotero-obsidian-bridge')) {
|
||||
suggestedSkills.push('zotero-obsidian-bridge');
|
||||
}
|
||||
if (/\b(paper|papers|literature|review|claim|method|evidence)\b|论文|综述|paper/i.test(userPrompt) &&
|
||||
SKILL_LIST.includes('obsidian-literature-workflow')) {
|
||||
suggestedSkills.push('obsidian-literature-workflow');
|
||||
}
|
||||
}
|
||||
|
||||
const dedupedSuggestedSkills = [...new Set(suggestedSkills)];
|
||||
|
||||
// Format grouped skills (skip empty groups)
|
||||
const groupedDisplay = Object.entries(SKILL_GROUPS)
|
||||
.filter(([, skills]) => skills.length > 0)
|
||||
.map(([cat, skills]) => `[${cat}] ${skills.join(', ')}`)
|
||||
.join('\n');
|
||||
|
||||
// Build suggested skills hint
|
||||
const suggestedHint = dedupedSuggestedSkills.length > 0
|
||||
? `\n**Pre-matched skills (MUST activate these)**: ${dedupedSuggestedSkills.join(', ')}\nThese skills matched keywords in the user's prompt. You MUST activate them via Skill tool.\n`
|
||||
: '';
|
||||
|
||||
const boundRepoHint = binding.bound && isResearchPrompt
|
||||
? `\n**Bound Obsidian repo detected**: ${binding.projectId || 'unknown-project'}\nUse lightweight curator behavior by default: keep \`Daily/YYYY-MM-DD.md\` and \`.claude/project-memory/<project_id>.md\` in sync when this turn changes research state, and touch \`00-Hub.md\` only when top-level project status really changes. Prefer \`obsidian-project-kb-core\` by default, and add \`obsidian-source-ingestion\`, \`obsidian-literature-workflow\`, or \`obsidian-kb-artifacts\` only when the task clearly needs them.\n`
|
||||
: '';
|
||||
|
||||
// Generate output
|
||||
const output = `## Instruction: Forced Skill Activation (Mandatory)
|
||||
|
||||
Silently scan the user's request against available skills. Do NOT list every skill with Yes/No.
|
||||
|
||||
Available skills:
|
||||
${groupedDisplay}
|
||||
${suggestedHint}${boundRepoHint}
|
||||
**Action**:
|
||||
- If any skill matches → Activate via Skill tool, then output: "Activating: [skill-name] — [reason]"
|
||||
- If no skill matches → Output: "No skills needed"
|
||||
- Begin implementation only after activation is complete.
|
||||
- When multiple skills match, activate all of them.
|
||||
`;
|
||||
|
||||
console.log(output);
|
||||
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user