Initial commit
This commit is contained in:
790
hooks/puerto-prompt-analyzer.js
Executable file
790
hooks/puerto-prompt-analyzer.js
Executable file
@@ -0,0 +1,790 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Puerto Prompt Analyzer Hook for Claude Code (v2.0)
|
||||
*
|
||||
* Analyzes user prompts before Claude processes them, providing:
|
||||
* - Task type classification (research, implementation, mixed)
|
||||
* - Instruction quality validation
|
||||
* - Intelligent plugin recommendations from Puerto marketplace
|
||||
* - Project context awareness
|
||||
* - Caching and performance optimization
|
||||
*
|
||||
* Part of the essentials plugin
|
||||
* @see plugins/essentials/hooks/README.md for documentation
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
const RESEARCH_KEYWORDS = [
|
||||
'explain', 'analyze', 'research', 'investigate', 'understand',
|
||||
'review', 'compare', 'summarize', 'describe', 'document',
|
||||
'learn', 'study', 'explore', 'examine', 'evaluate'
|
||||
];
|
||||
|
||||
const IMPLEMENTATION_KEYWORDS = [
|
||||
'implement', 'create', 'build', 'fix', 'refactor', 'add',
|
||||
'modify', 'write', 'develop', 'code', 'make', 'update',
|
||||
'change', 'remove', 'delete', 'optimize', 'improve', 'deploy'
|
||||
];
|
||||
|
||||
const STOP_WORDS = new Set([
|
||||
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
|
||||
'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'be', 'been',
|
||||
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should',
|
||||
'could', 'may', 'might', 'can', 'this', 'that', 'these', 'those'
|
||||
]);
|
||||
|
||||
const VALIDATION_PATTERNS = {
|
||||
tooVague: {
|
||||
patterns: [/^(fix it|make it better|improve this|do it|help)$/i],
|
||||
suggestion: 'Be more specific about what you want to fix or improve'
|
||||
},
|
||||
missingContext: {
|
||||
patterns: [/\b(this|that|it)\b/i],
|
||||
minLength: 30,
|
||||
suggestion: 'Provide clear context - what specifically does "this" or "that" refer to?'
|
||||
},
|
||||
overlyBroad: {
|
||||
patterns: [/^(build an? app|create a (website|system)|make (something|a thing))$/i],
|
||||
suggestion: 'Break down your request into specific features or components'
|
||||
}
|
||||
};
|
||||
|
||||
// Scoring weights (tuned for better results)
|
||||
const WEIGHTS = {
|
||||
keywordMatch: 3,
|
||||
nameMatch: 5,
|
||||
descriptionOverlap: 2,
|
||||
categoryMatch: 4,
|
||||
taskTypeAlign: 5,
|
||||
projectTypeMatch: 3
|
||||
};
|
||||
|
||||
const MIN_SCORE_THRESHOLD = 8;
|
||||
const MAX_RECOMMENDATIONS = 3;
|
||||
const CACHE_TTL = 60000; // 60 seconds
|
||||
const SESSION_MEMORY_TTL = 3600000; // 1 hour
|
||||
|
||||
// ============================================================================
|
||||
// CACHING & SESSION MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
const CACHE = {
|
||||
marketplace: null,
|
||||
marketplacePath: null,
|
||||
timestamp: 0,
|
||||
installedPlugins: null,
|
||||
installedTimestamp: 0
|
||||
};
|
||||
|
||||
const SESSION_MEMORY = new Map(); // session_id -> { shown: Set, timestamp }
|
||||
|
||||
function cleanupOldSessions() {
|
||||
const now = Date.now();
|
||||
for (const [sessionId, data] of SESSION_MEMORY.entries()) {
|
||||
if (now - data.timestamp > SESSION_MEMORY_TTL) {
|
||||
SESSION_MEMORY.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionMemory(sessionId) {
|
||||
if (!SESSION_MEMORY.has(sessionId)) {
|
||||
SESSION_MEMORY.set(sessionId, {
|
||||
shown: new Set(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
return SESSION_MEMORY.get(sessionId);
|
||||
}
|
||||
|
||||
function markAsShown(sessionId, pluginName) {
|
||||
const memory = getSessionMemory(sessionId);
|
||||
memory.shown.add(pluginName);
|
||||
memory.timestamp = Date.now();
|
||||
}
|
||||
|
||||
function wasRecentlyShown(sessionId, pluginName) {
|
||||
const memory = SESSION_MEMORY.get(sessionId);
|
||||
return memory && memory.shown.has(pluginName);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN ENTRY POINT
|
||||
// ============================================================================
|
||||
|
||||
function main() {
|
||||
const perfStart = Date.now();
|
||||
|
||||
try {
|
||||
// Cleanup old sessions periodically
|
||||
if (Math.random() < 0.1) { // 10% chance
|
||||
cleanupOldSessions();
|
||||
}
|
||||
|
||||
// Read hook input from stdin
|
||||
const input = fs.readFileSync(0, 'utf-8');
|
||||
const hookInput = JSON.parse(input);
|
||||
|
||||
// Analyze and generate output
|
||||
const result = analyzeInstruction(hookInput);
|
||||
|
||||
// Output JSON to stdout
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
// Performance logging
|
||||
const elapsed = Date.now() - perfStart;
|
||||
if (elapsed > 500) {
|
||||
console.error(`[puerto-prompt-analyzer] SLOW execution: ${elapsed}ms`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Fail open - log error but allow prompt to proceed
|
||||
console.error('[puerto-prompt-analyzer] Fatal error:', error.message);
|
||||
console.log(JSON.stringify(allowPrompt()));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CORE ANALYSIS LOGIC
|
||||
// ============================================================================
|
||||
|
||||
function analyzeInstruction(hookInput) {
|
||||
try {
|
||||
const { prompt, cwd, session_id } = hookInput;
|
||||
|
||||
// Skip if empty or command
|
||||
if (!prompt || !prompt.trim()) {
|
||||
return allowPrompt();
|
||||
}
|
||||
|
||||
if (prompt.trim().startsWith('/')) {
|
||||
return allowPrompt();
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
const config = loadConfiguration();
|
||||
|
||||
// Classify task type
|
||||
const taskType = classifyTaskType(prompt);
|
||||
|
||||
// Detect project context
|
||||
const projectContext = detectProjectContext(cwd);
|
||||
|
||||
// Validate instruction quality
|
||||
const validation = validateInstruction(prompt);
|
||||
|
||||
// Load and score plugins
|
||||
const recommendations = getPluginRecommendations(
|
||||
prompt,
|
||||
taskType,
|
||||
cwd,
|
||||
session_id,
|
||||
projectContext,
|
||||
config
|
||||
);
|
||||
|
||||
// Mark shown plugins
|
||||
recommendations.forEach(p => markAsShown(session_id, p.name));
|
||||
|
||||
// Generate markdown output
|
||||
const additionalContext = formatRecommendations(
|
||||
taskType,
|
||||
recommendations,
|
||||
validation,
|
||||
projectContext
|
||||
);
|
||||
|
||||
return {
|
||||
decision: undefined,
|
||||
reason: 'Analysis complete',
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'UserPromptSubmit',
|
||||
additionalContext
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[puerto-prompt-analyzer] Error during analysis:', error.message);
|
||||
return allowPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
function loadConfiguration() {
|
||||
const configPath = path.join(os.homedir(), '.claude', 'puerto-prompt-analyzer.json');
|
||||
|
||||
const defaults = {
|
||||
minScore: MIN_SCORE_THRESHOLD,
|
||||
maxRecommendations: MAX_RECOMMENDATIONS,
|
||||
cacheMinutes: 1,
|
||||
blacklist: [],
|
||||
favoriteCategories: [],
|
||||
showScores: false
|
||||
};
|
||||
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
return { ...defaults, ...userConfig };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[puerto-prompt-analyzer] Error loading config:', error.message);
|
||||
}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROJECT CONTEXT DETECTION
|
||||
// ============================================================================
|
||||
|
||||
function detectProjectContext(cwd) {
|
||||
if (!cwd) return { type: 'unknown', files: [] };
|
||||
|
||||
const context = {
|
||||
type: 'unknown',
|
||||
languages: [],
|
||||
frameworks: [],
|
||||
files: []
|
||||
};
|
||||
|
||||
try {
|
||||
// Check for package.json (JavaScript/Node.js)
|
||||
if (fs.existsSync(path.join(cwd, 'package.json'))) {
|
||||
context.type = 'javascript';
|
||||
context.languages.push('javascript', 'nodejs');
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
|
||||
|
||||
// Detect frameworks
|
||||
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
||||
if (deps['react']) context.frameworks.push('react');
|
||||
if (deps['vue']) context.frameworks.push('vue');
|
||||
if (deps['next']) context.frameworks.push('nextjs');
|
||||
if (deps['express']) context.frameworks.push('express');
|
||||
}
|
||||
|
||||
// Check for Python
|
||||
if (fs.existsSync(path.join(cwd, 'requirements.txt')) ||
|
||||
fs.existsSync(path.join(cwd, 'pyproject.toml'))) {
|
||||
context.type = 'python';
|
||||
context.languages.push('python');
|
||||
}
|
||||
|
||||
// Check for Rust
|
||||
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) {
|
||||
context.type = 'rust';
|
||||
context.languages.push('rust');
|
||||
}
|
||||
|
||||
// Check for Go
|
||||
if (fs.existsSync(path.join(cwd, 'go.mod'))) {
|
||||
context.type = 'go';
|
||||
context.languages.push('go', 'golang');
|
||||
}
|
||||
|
||||
// Check for Ruby
|
||||
if (fs.existsSync(path.join(cwd, 'Gemfile'))) {
|
||||
context.type = 'ruby';
|
||||
context.languages.push('ruby');
|
||||
}
|
||||
|
||||
// Check for Java/Kotlin
|
||||
if (fs.existsSync(path.join(cwd, 'pom.xml')) ||
|
||||
fs.existsSync(path.join(cwd, 'build.gradle'))) {
|
||||
context.type = 'java';
|
||||
context.languages.push('java');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[puerto-prompt-analyzer] Error detecting project:', error.message);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEXT PROCESSING UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
function stem(word) {
|
||||
// Simple stemmer - remove common suffixes
|
||||
return word
|
||||
.replace(/ies$/, 'y')
|
||||
.replace(/ing$/, '')
|
||||
.replace(/ed$/, '')
|
||||
.replace(/s$/, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function tokenize(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, ' ') // Keep hyphens
|
||||
.split(/\s+/)
|
||||
.map(stem)
|
||||
.filter(w => w.length > 2 && !STOP_WORDS.has(w));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TASK CLASSIFICATION
|
||||
// ============================================================================
|
||||
|
||||
function classifyTaskType(prompt) {
|
||||
const tokens = tokenize(prompt);
|
||||
const lower = prompt.toLowerCase();
|
||||
|
||||
// Count keyword matches (using stemmed tokens)
|
||||
const researchScore = RESEARCH_KEYWORDS.filter(kw =>
|
||||
tokens.includes(stem(kw)) || lower.includes(kw)
|
||||
).length;
|
||||
|
||||
const implScore = IMPLEMENTATION_KEYWORDS.filter(kw =>
|
||||
tokens.includes(stem(kw)) || lower.includes(kw)
|
||||
).length;
|
||||
|
||||
// Enhanced classification
|
||||
if (implScore > researchScore * 1.5) {
|
||||
return 'Implementation';
|
||||
}
|
||||
if (researchScore > implScore * 1.5) {
|
||||
return 'Research';
|
||||
}
|
||||
if (researchScore > 0 && implScore > 0) {
|
||||
return 'Mixed';
|
||||
}
|
||||
|
||||
return 'General';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INSTRUCTION VALIDATION
|
||||
// ============================================================================
|
||||
|
||||
function validateInstruction(prompt) {
|
||||
const suggestions = [];
|
||||
|
||||
// Check for vague instructions
|
||||
if (VALIDATION_PATTERNS.tooVague.patterns.some(p => p.test(prompt))) {
|
||||
suggestions.push(VALIDATION_PATTERNS.tooVague.suggestion);
|
||||
}
|
||||
|
||||
// Check for missing context (only if prompt is very short)
|
||||
const minLength = VALIDATION_PATTERNS.missingContext.minLength;
|
||||
if (prompt.length < minLength &&
|
||||
VALIDATION_PATTERNS.missingContext.patterns.some(p => p.test(prompt))) {
|
||||
suggestions.push(VALIDATION_PATTERNS.missingContext.suggestion);
|
||||
}
|
||||
|
||||
// Check for overly broad requests
|
||||
if (VALIDATION_PATTERNS.overlyBroad.patterns.some(p => p.test(prompt))) {
|
||||
suggestions.push(VALIDATION_PATTERNS.overlyBroad.suggestion);
|
||||
}
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INSTALLED PLUGINS DETECTION
|
||||
// ============================================================================
|
||||
|
||||
function getInstalledPlugins() {
|
||||
const now = Date.now();
|
||||
|
||||
// Use cache
|
||||
if (CACHE.installedPlugins && (now - CACHE.installedTimestamp) < CACHE_TTL) {
|
||||
return CACHE.installedPlugins;
|
||||
}
|
||||
|
||||
const installed = new Set();
|
||||
|
||||
try {
|
||||
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
||||
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
||||
|
||||
if (settings.enabledPlugins) {
|
||||
Object.keys(settings.enabledPlugins).forEach(pluginId => {
|
||||
// Extract plugin name from "plugin@marketplace" format
|
||||
const pluginName = pluginId.split('@')[0];
|
||||
installed.add(pluginName);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[puerto-prompt-analyzer] Error reading installed plugins:', error.message);
|
||||
}
|
||||
|
||||
CACHE.installedPlugins = installed;
|
||||
CACHE.installedTimestamp = now;
|
||||
|
||||
return installed;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLUGIN RECOMMENDATIONS
|
||||
// ============================================================================
|
||||
|
||||
function getPluginRecommendations(prompt, taskType, cwd, sessionId, projectContext, config) {
|
||||
try {
|
||||
// Find and load marketplace.json (with caching)
|
||||
const marketplacePath = findMarketplaceJson(cwd);
|
||||
|
||||
if (!marketplacePath) {
|
||||
console.error('[puerto-prompt-analyzer] marketplace.json not found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const marketplace = getMarketplaceData(marketplacePath);
|
||||
|
||||
if (!marketplace || !marketplace.plugins || !Array.isArray(marketplace.plugins)) {
|
||||
console.error('[puerto-prompt-analyzer] Invalid marketplace format');
|
||||
return [];
|
||||
}
|
||||
|
||||
const installedPlugins = getInstalledPlugins();
|
||||
|
||||
// Score all plugins
|
||||
const scored = marketplace.plugins
|
||||
.map(plugin => ({
|
||||
...plugin,
|
||||
score: scorePlugin(plugin, prompt, taskType, sessionId, projectContext, installedPlugins, config)
|
||||
}))
|
||||
.filter(p => p.score >= config.minScore) // Quality threshold
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
// Apply diversity (don't show too many similar plugins)
|
||||
const diverse = diversifyRecommendations(scored, config.maxRecommendations);
|
||||
|
||||
// Add recommendation reasons
|
||||
return diverse.map(plugin => ({
|
||||
...plugin,
|
||||
reason: generateReason(plugin, taskType, prompt, projectContext)
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
console.error('[puerto-prompt-analyzer] Error loading marketplace:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getMarketplaceData(marketplacePath) {
|
||||
const now = Date.now();
|
||||
|
||||
// Check cache
|
||||
if (CACHE.marketplace &&
|
||||
CACHE.marketplacePath === marketplacePath &&
|
||||
(now - CACHE.timestamp) < CACHE_TTL) {
|
||||
return CACHE.marketplace;
|
||||
}
|
||||
|
||||
// Load and cache
|
||||
try {
|
||||
CACHE.marketplace = JSON.parse(fs.readFileSync(marketplacePath, 'utf-8'));
|
||||
CACHE.marketplacePath = marketplacePath;
|
||||
CACHE.timestamp = now;
|
||||
return CACHE.marketplace;
|
||||
} catch (error) {
|
||||
console.error('[puerto-prompt-analyzer] Error parsing marketplace:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function findMarketplaceJson(startDir) {
|
||||
if (!startDir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let currentDir = startDir;
|
||||
|
||||
// Search up to 5 levels up
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const marketplacePath = path.join(currentDir, '.claude-plugin', 'marketplace.json');
|
||||
|
||||
if (fs.existsSync(marketplacePath)) {
|
||||
return marketplacePath;
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) {
|
||||
break; // Reached root
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function scorePlugin(plugin, prompt, taskType, sessionId, projectContext, installedPlugins, config) {
|
||||
const promptTokens = new Set(tokenize(prompt));
|
||||
const lower = prompt.toLowerCase();
|
||||
let score = 0;
|
||||
|
||||
// Skip essentials plugin (it's already installed by definition)
|
||||
if (plugin.name === 'essentials') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Skip blacklisted plugins
|
||||
if (config.blacklist.includes(plugin.name)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Skip installed plugins
|
||||
if (installedPlugins.has(plugin.name)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Reduce score for recently shown plugins
|
||||
if (wasRecentlyShown(sessionId, plugin.name)) {
|
||||
score -= 5; // Penalty for repetition
|
||||
}
|
||||
|
||||
// 1. Tokenized description overlap (better than simple word matching)
|
||||
if (plugin.description) {
|
||||
const descTokens = tokenize(plugin.description);
|
||||
const overlap = descTokens.filter(t => promptTokens.has(t)).length;
|
||||
score += overlap * WEIGHTS.descriptionOverlap;
|
||||
}
|
||||
|
||||
// 2. Name match (strong signal)
|
||||
if (plugin.name) {
|
||||
const nameWords = plugin.name.split('-');
|
||||
const nameMatches = nameWords.filter(w =>
|
||||
promptTokens.has(stem(w)) || lower.includes(w)
|
||||
).length;
|
||||
score += nameMatches * WEIGHTS.nameMatch;
|
||||
}
|
||||
|
||||
// 3. Keywords match
|
||||
if (plugin.keywords && Array.isArray(plugin.keywords)) {
|
||||
const keywordMatches = plugin.keywords.filter(kw =>
|
||||
promptTokens.has(stem(kw)) || lower.includes(kw.toLowerCase())
|
||||
).length;
|
||||
score += keywordMatches * WEIGHTS.keywordMatch;
|
||||
}
|
||||
|
||||
// 4. Task type alignment
|
||||
if (taskType === 'Implementation') {
|
||||
if (plugin.description && /agent|specialist|builder|creator|developer/.test(plugin.description.toLowerCase())) {
|
||||
score += WEIGHTS.taskTypeAlign;
|
||||
}
|
||||
}
|
||||
|
||||
if (taskType === 'Research') {
|
||||
if (plugin.description && /skill|knowledge|guide|reference|documentation/.test(plugin.description.toLowerCase())) {
|
||||
score += WEIGHTS.taskTypeAlign;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Project context matching
|
||||
if (projectContext.type !== 'unknown') {
|
||||
const contextTerms = [
|
||||
...projectContext.languages,
|
||||
...projectContext.frameworks,
|
||||
projectContext.type
|
||||
];
|
||||
|
||||
contextTerms.forEach(term => {
|
||||
if (plugin.keywords && plugin.keywords.some(kw => kw.toLowerCase().includes(term))) {
|
||||
score += WEIGHTS.projectTypeMatch;
|
||||
}
|
||||
if (plugin.description && plugin.description.toLowerCase().includes(term)) {
|
||||
score += WEIGHTS.projectTypeMatch * 0.5;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Category boost for favorites
|
||||
if (config.favoriteCategories.length > 0 && plugin.category) {
|
||||
if (config.favoriteCategories.includes(plugin.category)) {
|
||||
score += 3;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.max(0, score); // Never negative
|
||||
}
|
||||
|
||||
function diversifyRecommendations(plugins, maxCount) {
|
||||
const selected = [];
|
||||
const usedCategories = new Set();
|
||||
const usedKeywords = new Set();
|
||||
|
||||
// First pass: pick best from different categories
|
||||
for (const plugin of plugins) {
|
||||
if (selected.length >= maxCount) break;
|
||||
|
||||
const category = plugin.category || 'general';
|
||||
const primaryKeyword = (plugin.keywords && plugin.keywords[0]) || '';
|
||||
|
||||
// Prefer different categories and keywords
|
||||
if (!usedCategories.has(category) || selected.length === 0) {
|
||||
selected.push(plugin);
|
||||
usedCategories.add(category);
|
||||
if (primaryKeyword) usedKeywords.add(primaryKeyword);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: fill remaining slots if needed
|
||||
if (selected.length < maxCount) {
|
||||
for (const plugin of plugins) {
|
||||
if (selected.length >= maxCount) break;
|
||||
if (!selected.includes(plugin)) {
|
||||
selected.push(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
function generateReason(plugin, taskType, prompt, projectContext) {
|
||||
const reasons = [];
|
||||
|
||||
// Check for strong keyword matches
|
||||
if (plugin.keywords && Array.isArray(plugin.keywords)) {
|
||||
const matches = plugin.keywords.filter(kw =>
|
||||
prompt.toLowerCase().includes(kw.toLowerCase())
|
||||
);
|
||||
if (matches.length > 0) {
|
||||
reasons.push(`Matches keywords: ${matches.slice(0, 2).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for name matches
|
||||
const nameWords = plugin.name.split('-');
|
||||
const nameMatches = nameWords.filter(w =>
|
||||
prompt.toLowerCase().includes(w)
|
||||
);
|
||||
if (nameMatches.length > 0) {
|
||||
reasons.push(`Related to ${nameMatches.join(', ')}`);
|
||||
}
|
||||
|
||||
// Project context match
|
||||
if (projectContext.type !== 'unknown') {
|
||||
const contextTerms = [...projectContext.languages, ...projectContext.frameworks];
|
||||
const matches = contextTerms.filter(term =>
|
||||
(plugin.description && plugin.description.toLowerCase().includes(term)) ||
|
||||
(plugin.keywords && plugin.keywords.some(kw => kw.toLowerCase().includes(term)))
|
||||
);
|
||||
|
||||
if (matches.length > 0) {
|
||||
reasons.push(`Fits your ${projectContext.type} project`);
|
||||
}
|
||||
}
|
||||
|
||||
// Task type alignment
|
||||
if (taskType === 'Implementation') {
|
||||
reasons.push('Provides specialized implementation tools');
|
||||
} else if (taskType === 'Research') {
|
||||
reasons.push('Offers expert knowledge and guidance');
|
||||
}
|
||||
|
||||
// Default reason if nothing specific
|
||||
if (reasons.length === 0) {
|
||||
reasons.push('Relevant to your task based on description');
|
||||
}
|
||||
|
||||
return reasons[0]; // Return the most specific reason
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OUTPUT FORMATTING
|
||||
// ============================================================================
|
||||
|
||||
function formatRecommendations(taskType, plugins, validation, projectContext) {
|
||||
let md = '\n\n---\n\n## 🔍 Puerto Prompt Analysis\n\n';
|
||||
|
||||
// Task type
|
||||
md += `**Task Type:** ${taskType}`;
|
||||
|
||||
// Project context if detected
|
||||
if (projectContext.type !== 'unknown') {
|
||||
md += ` | **Project:** ${projectContext.type}`;
|
||||
if (projectContext.frameworks.length > 0) {
|
||||
md += ` (${projectContext.frameworks.slice(0, 2).join(', ')})`;
|
||||
}
|
||||
}
|
||||
md += '\n';
|
||||
|
||||
// Validation suggestions
|
||||
if (validation.suggestions.length > 0) {
|
||||
md += `\n**💡 Suggestions:**\n`;
|
||||
validation.suggestions.forEach(s => {
|
||||
md += `- ${s}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
// Plugin recommendations
|
||||
if (plugins.length === 0) {
|
||||
md += '\n*No specific plugin recommendations found.*\n';
|
||||
md += '\n---\n\n';
|
||||
return md;
|
||||
}
|
||||
|
||||
md += `\n**📦 Recommended Plugins:**\n\n`;
|
||||
|
||||
plugins.forEach((plugin, idx) => {
|
||||
md += `### ${idx + 1}. \`${plugin.name}\`\n`;
|
||||
md += `**Description:** ${plugin.description}\n`;
|
||||
md += `**Why:** ${plugin.reason}\n`;
|
||||
|
||||
// Show score if configured
|
||||
const config = loadConfiguration();
|
||||
if (config.showScores) {
|
||||
md += `**Score:** ${Math.round(plugin.score)}\n`;
|
||||
}
|
||||
|
||||
md += `**Install:** \`/plugin install ${plugin.name}\`\n\n`;
|
||||
});
|
||||
|
||||
md += '---\n\n';
|
||||
return md;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
function allowPrompt() {
|
||||
return {
|
||||
decision: undefined,
|
||||
reason: 'Proceeding normally',
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'UserPromptSubmit',
|
||||
additionalContext: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RUN
|
||||
// ============================================================================
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
// Export for testing
|
||||
module.exports = {
|
||||
classifyTaskType,
|
||||
validateInstruction,
|
||||
scorePlugin,
|
||||
analyzeInstruction,
|
||||
tokenize,
|
||||
stem,
|
||||
detectProjectContext
|
||||
};
|
||||
Reference in New Issue
Block a user