@tosin2013/mcp-shrimp-task-manager
Version:
Enhanced MCP Shrimp Task Manager with comprehensive LLM integration. A task management tool built for AI Agents, emphasizing chain-of-thought, reflection, and style consistency. Features real GPT-4 ↔ MCP tools communication, comprehensive testing pipeline
228 lines • 9.6 kB
JavaScript
/**
* Search Indexer for the Idea Honing Tool
*
* This utility provides search and discovery functionality for specifications,
* allowing users to find and browse existing specifications.
*/
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { loadSpecification, listSpecifications } from './file-system.js';
// Define the directory for the search index
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const SEARCH_INDEX_DIR = path.join(__dirname, '../../../../data/idea-honing/search-index');
/**
* Builds the search index for all specifications
*
* @returns Promise that resolves when the index is built
*/
export async function buildSearchIndex() {
try {
// Ensure the search index directory exists
if (!fs.existsSync(SEARCH_INDEX_DIR)) {
fs.mkdirSync(SEARCH_INDEX_DIR, { recursive: true });
}
// Get all specifications
const specificationIds = await listSpecifications();
// Process each specification
for (const specId of specificationIds) {
try {
// Load the specification
const specification = await loadSpecification(specId);
// Index the specification
await indexSpecification(specification);
}
catch (error) {
console.error(`Error indexing specification ${specId}:`, error);
}
}
console.log(`Search index built with ${specificationIds.length} specifications`);
}
catch (error) {
throw new Error(`Failed to build search index: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Indexes a single specification
*
* @param specification - Specification to index
* @returns Promise that resolves when the specification is indexed
*/
export async function indexSpecification(specification) {
try {
// Extract the overview section for the description
const overviewSection = specification.sections.find(s => s.title.toLowerCase().includes('overview'));
// Extract keywords from the specification content
const keywords = extractKeywords(specification.title + ' ' +
(overviewSection ? overviewSection.content : '') + ' ' +
specification.sections.map(s => s.title).join(' '));
// Create the search index entry
const indexEntry = {
id: specification.id,
title: specification.title,
description: overviewSection ? overviewSection.content.substring(0, 200) + '...' : '',
projectId: specification.projectId,
featureId: specification.featureId,
status: specification.metadata.status,
authors: specification.metadata.authors,
tags: specification.metadata.tags,
createdAt: specification.createdAt.toISOString(),
updatedAt: specification.updatedAt.toISOString(),
sectionTitles: specification.sections.map(s => s.title),
sectionContent: specification.sections.map(s => s.content).join(' '),
keywords
};
// Save the index entry
const indexPath = path.join(SEARCH_INDEX_DIR, `${specification.id}.json`);
await fs.promises.writeFile(indexPath, JSON.stringify(indexEntry, null, 2), 'utf-8');
}
catch (error) {
throw new Error(`Failed to index specification: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Extracts keywords from text
*
* @param text - Text to extract keywords from
* @returns Array of keywords
*/
function extractKeywords(text) {
// Split the text into words
const words = text.toLowerCase()
.replace(/[^\w\s]/g, ' ') // Replace punctuation with spaces
.split(/\s+/) // Split on whitespace
.filter(word => word.length > 3); // Filter out short words
// Remove common words
const commonWords = ['the', 'and', 'for', 'with', 'that', 'this', 'from', 'will', 'have', 'been'];
const filteredWords = words.filter(word => !commonWords.includes(word));
// Count word frequency
const wordCounts = new Map();
for (const word of filteredWords) {
wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
}
// Sort by frequency
const sortedWords = [...wordCounts.entries()]
.sort((a, b) => b[1] - a[1])
.map(entry => entry[0]);
// Return the top 10 keywords
return sortedWords.slice(0, 10);
}
/**
* Searches for specifications matching a query
*
* @param query - Search query
* @param filters - Optional search filters
* @returns Promise that resolves with search results
*/
export async function searchSpecifications(query, filters) {
try {
// Ensure the search index directory exists
if (!fs.existsSync(SEARCH_INDEX_DIR)) {
return [];
}
// Get all index files
const indexFiles = await fs.promises.readdir(SEARCH_INDEX_DIR);
// Process each index file
const results = [];
for (const indexFile of indexFiles) {
try {
// Read the index file
const indexPath = path.join(SEARCH_INDEX_DIR, indexFile);
const indexContent = await fs.promises.readFile(indexPath, 'utf-8');
const indexEntry = JSON.parse(indexContent);
// Apply filters
if (filters) {
if (filters.projectId && indexEntry.projectId !== filters.projectId) {
continue;
}
if (filters.featureId && indexEntry.featureId !== filters.featureId) {
continue;
}
if (filters.status && indexEntry.status !== filters.status) {
continue;
}
if (filters.author && !indexEntry.authors.includes(filters.author)) {
continue;
}
if (filters.tag && !indexEntry.tags.includes(filters.tag)) {
continue;
}
if (filters.startDate && new Date(indexEntry.createdAt) < new Date(filters.startDate)) {
continue;
}
if (filters.endDate && new Date(indexEntry.createdAt) > new Date(filters.endDate)) {
continue;
}
}
// Skip if query is empty and no filters are applied
if (!query && !filters) {
continue;
}
// Calculate relevance score
let relevance = 0;
const matchingSections = [];
const matchingKeywords = [];
if (query) {
// Normalize the query
const normalizedQuery = query.toLowerCase();
const queryWords = normalizedQuery.split(/\s+/);
// Check title match
if (indexEntry.title.toLowerCase().includes(normalizedQuery)) {
relevance += 0.5;
}
// Check description match
if (indexEntry.description.toLowerCase().includes(normalizedQuery)) {
relevance += 0.3;
}
// Check section titles
for (const sectionTitle of indexEntry.sectionTitles) {
if (sectionTitle.toLowerCase().includes(normalizedQuery)) {
relevance += 0.2;
matchingSections.push(sectionTitle);
}
}
// Check keyword match
for (const keyword of indexEntry.keywords) {
if (queryWords.includes(keyword)) {
relevance += 0.1;
matchingKeywords.push(keyword);
}
}
// Check content match
if (indexEntry.sectionContent.toLowerCase().includes(normalizedQuery)) {
relevance += 0.1;
}
}
else {
// If no query but filters match, give a base relevance
relevance = 0.5;
}
// Add to results if relevant
if (relevance > 0) {
results.push({
id: indexEntry.id,
title: indexEntry.title,
description: indexEntry.description,
projectId: indexEntry.projectId,
featureId: indexEntry.featureId,
status: indexEntry.status,
relevance,
matchingSections,
matchingKeywords
});
}
}
catch (error) {
console.error(`Error processing index file ${indexFile}:`, error);
}
}
// Sort by relevance
results.sort((a, b) => b.relevance - a.relevance);
return results;
}
catch (error) {
throw new Error(`Failed to search specifications: ${error instanceof Error ? error.message : String(error)}`);
}
}
//# sourceMappingURL=search-indexer.js.map