UNPKG

@shirokuma-library/mcp-knowledge-base

Version:

MCP server for AI-powered knowledge management with semantic search, graph analysis, and automatic enrichment

268 lines (267 loc) • 10 kB
#!/usr/bin/env node import { Command } from 'commander'; import chalk from 'chalk'; import Table from 'cli-table3'; import { AppDataSource } from '../data-source.js'; import { ItemRepository } from '../repositories/ItemRepository.js'; import { runMigration } from './commands/migrate.js'; import { createExportCommand } from './commands/export.js'; import { importData } from './commands/import.js'; import { createConfigCommand } from './commands/config.js'; import { Item } from '../entities/Item.js'; import { Status } from '../entities/Status.js'; import { Tag } from '../entities/Tag.js'; import { ItemTag } from '../entities/ItemTag.js'; const program = new Command(); program .name('shirokuma-kb') .description('Shirokuma Knowledge Base CLI (TypeORM)') .version('0.9.0'); async function ensureDb() { if (!AppDataSource.isInitialized) { await AppDataSource.initialize(); } } program .command('create') .description('Create a new item') .requiredOption('-t, --type <type>', 'Item type') .requiredOption('-T, --title <title>', 'Item title') .option('-d, --description <description>', 'Item description') .option('-c, --content <content>', 'Item content') .option('-s, --status <status>', 'Status', 'Open') .option('-p, --priority <priority>', 'Priority', 'MEDIUM') .option('--tags <tags>', 'Comma-separated tags') .action(async (options) => { try { await ensureDb(); const statusRepo = AppDataSource.getRepository(Status); let status = await statusRepo.findOne({ where: { name: options.status } }); if (!status) { status = await statusRepo.save({ name: options.status, isClosable: false, sortOrder: 0 }); } const itemRepo = new ItemRepository(); const item = await itemRepo.create({ type: options.type, title: options.title, description: options.description || '', content: options.content || '', statusId: status.id, priority: options.priority }); if (options.tags) { const tagRepo = AppDataSource.getRepository(Tag); const itemTagRepo = AppDataSource.getRepository(ItemTag); const tagNames = options.tags.split(',').map((t) => t.trim()); for (const tagName of tagNames) { let tag = await tagRepo.findOne({ where: { name: tagName } }); if (!tag) { tag = await tagRepo.save({ name: tagName }); } await itemTagRepo.save({ itemId: item.id, tagId: tag.id }); } } console.log(chalk.green(`āœ… Created item #${item.id}: ${item.title}`)); await AppDataSource.destroy(); } catch (error) { console.error(chalk.red('Error:'), error); process.exit(1); } }); program .command('list') .description('List items') .option('-t, --type <type>', 'Filter by type') .option('-s, --status <status>', 'Filter by status') .option('-l, --limit <limit>', 'Limit results', '20') .action(async (options) => { try { await ensureDb(); const itemRepo = new ItemRepository(); const items = await itemRepo.findAll({ type: options.type, status: options.status, limit: parseInt(options.limit) }); if (items.length === 0) { console.log(chalk.yellow('No items found')); } else { const table = new Table({ head: ['ID', 'Type', 'Title', 'Status', 'Priority'], colWidths: [8, 15, 40, 15, 10] }); for (const item of items) { const statusRepo = AppDataSource.getRepository(Status); const status = await statusRepo.findOne({ where: { id: item.statusId } }); table.push([ item.id, item.type, item.title.substring(0, 38), status?.name || 'Unknown', item.priority ]); } console.log(table.toString()); console.log(chalk.gray(`\nTotal: ${items.length} items`)); } await AppDataSource.destroy(); } catch (error) { console.error(chalk.red('Error:'), error); process.exit(1); } }); program .command('get <id>') .description('Get an item by ID') .action(async (id) => { try { await ensureDb(); const itemRepo = new ItemRepository(); const item = await itemRepo.findById(parseInt(id)); if (!item) { console.log(chalk.yellow(`Item #${id} not found`)); } else { const statusRepo = AppDataSource.getRepository(Status); const status = await statusRepo.findOne({ where: { id: item.statusId } }); const itemTagRepo = AppDataSource.getRepository(ItemTag); const tagRepo = AppDataSource.getRepository(Tag); const itemTags = await itemTagRepo.find({ where: { itemId: item.id } }); const tags = []; for (const it of itemTags) { const tag = await tagRepo.findOne({ where: { id: it.tagId } }); if (tag) tags.push(tag.name); } console.log(chalk.bold.cyan(`\nšŸ“„ Item #${item.id}\n`)); console.log(chalk.bold('Title:'), item.title); console.log(chalk.bold('Type:'), item.type); console.log(chalk.bold('Status:'), status?.name || 'Unknown'); console.log(chalk.bold('Priority:'), item.priority); if (tags.length > 0) { console.log(chalk.bold('Tags:'), tags.join(', ')); } if (item.description) { console.log(chalk.bold('\nDescription:')); console.log(item.description); } if (item.content) { console.log(chalk.bold('\nContent:')); console.log(item.content); } console.log(chalk.gray(`\nCreated: ${item.createdAt}`)); console.log(chalk.gray(`Updated: ${item.updatedAt}`)); } await AppDataSource.destroy(); } catch (error) { console.error(chalk.red('Error:'), error); process.exit(1); } }); program .command('search <query>') .description('Search items') .action(async (query) => { try { await ensureDb(); const itemRepo = new ItemRepository(); const items = await itemRepo.search(query); if (items.length === 0) { console.log(chalk.yellow('No items found')); } else { console.log(chalk.bold.cyan(`\nšŸ” Search Results (${items.length} items)\n`)); for (const item of items) { console.log(chalk.cyan(`[${item.id}]`), chalk.bold(item.title)); console.log(chalk.gray(` Type: ${item.type} | Priority: ${item.priority}`)); if (item.description) { const preview = item.description.substring(0, 80); console.log(chalk.gray(` ${preview}${item.description.length > 80 ? '...' : ''}`)); } console.log(); } } await AppDataSource.destroy(); } catch (error) { console.error(chalk.red('Error:'), error); process.exit(1); } }); program .command('migrate') .description('Run database migrations') .option('--reset', 'Reset database before migration') .option('--seed', 'Seed database after migration') .action(async (options) => { await runMigration(options); }); program.addCommand(createExportCommand()); program.addCommand(createConfigCommand()); program .command('import <file>') .description('Import data from JSON file') .option('--clear', 'Clear existing data before import') .action(async (file, options) => { await importData({ file, ...options }); }); program .command('serve') .description('Start MCP server') .action(async () => { console.log(chalk.cyan('Starting MCP server (TypeORM)...')); console.log(chalk.gray('Use Ctrl+C to stop')); const { startServer } = await import('../mcp/server.js'); await startServer(); }); program .command('stats') .description('Show database statistics') .action(async () => { try { await ensureDb(); const itemRepo = new ItemRepository(); const statusRepo = AppDataSource.getRepository(Status); const tagRepo = AppDataSource.getRepository(Tag); const itemCount = await itemRepo.count(); const statusCount = await statusRepo.count(); const tagCount = await tagRepo.count(); console.log(chalk.bold.cyan('\nšŸ“Š Database Statistics\n')); const table = new Table({ head: ['Entity', 'Count'], colWidths: [20, 10] }); table.push(['Items', itemCount], ['Statuses', statusCount], ['Tags', tagCount]); console.log(table.toString()); const items = await AppDataSource.getRepository(Item).find(); const typeGroups = new Map(); for (const item of items) { typeGroups.set(item.type, (typeGroups.get(item.type) || 0) + 1); } if (typeGroups.size > 0) { console.log(chalk.bold('\nItems by Type:')); const typeTable = new Table({ head: ['Type', 'Count'], colWidths: [20, 10] }); for (const [type, count] of typeGroups) { typeTable.push([type, count]); } console.log(typeTable.toString()); } await AppDataSource.destroy(); } catch (error) { console.error(chalk.red('Error:'), error); process.exit(1); } }); program.parse();