UNPKG

lorehub

Version:

Capture and surface the collective wisdom of your codebase

260 lines (259 loc) 10.9 kB
import { Command } from 'commander'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { Database } from '../db/database.js'; import { getRealmInfo } from './utils/realm.js'; import { getDbPath } from './utils/db-config.js'; import { renderAddLore } from './commands/add.js'; import { renderBrowse } from './commands/browse.js'; import { renderRealmInfo } from './commands/realm.js'; import { renderExport } from './commands/export.js'; import { renderImport } from './commands/import.js'; import { migrateEmbeddingsCommand } from './commands/migrate-embeddings.js'; import { configCommand } from './commands/config.js'; import workspaceCommand from './commands/workspace.js'; import syncCommand from './commands/sync.js'; import { EmbeddingService } from '../core/embeddings.js'; import { ConfigManager } from '../core/config.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Read package.json for version const packageJson = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8')); export function createCLI() { const program = new Command(); program .name('lh') .description('LoreHub - Capture and query your realm\'s development knowledge') .version(packageJson.version) .addHelpText('after', ` Grid View Indicators: • = Current realm L = Living (active) ≈ = Similar count W = Whispered (draft) Dist = Semantic distance (0.00 = exact match, higher = less similar) P = Proclaimed (verified) A = Archived Lore Types: DEC = Decree (rules) REQ = Requirement ASS = Assumption RIS = Risk CON = Constraint LES = Lesson QUE = Quest `); // Add command program .command('add [content...]') .description('Add a new lore to your realm') .option('-t, --type <type>', 'Lore type (decree, lesson, assumption, etc.)') .option('-w, --why <reason>', 'Why this lore exists') .option('-s, --provinces <provinces>', 'Comma-separated list of provinces') .option('--sigils <sigils>', 'Comma-separated list of sigils') .option('-c, --confidence <number>', 'Confidence level (0-100)') .action(async (contentParts, options) => { const content = contentParts.join(' '); const config = ConfigManager.getInstance(); try { if (!content && !process.stdin.isTTY) { console.error('Error: No content provided'); process.exit(1); } // Use config default if confidence not specified const confidence = options.confidence ? parseInt(options.confidence, 10) : config.get('defaultConfidence'); await renderAddLore({ initialContent: content, type: options.type, why: options.why, provinces: options.provinces?.split(',').map((s) => s.trim()), sigils: options.sigils?.split(',').map((t) => t.trim()), confidence, }); } catch (error) { console.error('Error:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); // Browse command (new unified interface) program .command('browse [query]') .alias('b') .description('Browse and search lores with unified interface') .option('-t, --type <type>', 'Filter by lore type') .option('-s, --province <province>', 'Filter by province') .option('-r, --realm <path>', 'Filter by specific realm path') .option('--current', 'Browse only current realm') .option('-m, --mode <mode>', 'Search mode: literal, semantic, or hybrid') .action(async (query, options) => { try { await renderBrowse({ query, type: options.type, province: options.province, realmPath: options.realm, currentRealmOnly: options.current, searchMode: options.mode, }); } catch (error) { console.error('Error:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); // Realm command program .command('realm') .description('Show current realm information') .action(async () => { try { if (!process.stdin.isTTY) { // Non-interactive mode const dbPath = getDbPath(); const db = new Database(dbPath); const realmInfo = await getRealmInfo(process.cwd()); const realm = db.findRealmByPath(realmInfo.path); if (realm) { console.log(`\nRealm: ${realm.name}`); console.log(`Path: ${realm.path}`); console.log(`Type: ${realm.isMonorepo ? 'Monorepo' : 'Standard'}`); if (realm.provinces.length > 0) { console.log(`Provinces: ${realm.provinces.join(', ')}`); } if (realm.gitRemote) { console.log(`Git: ${realm.gitRemote}`); } console.log(`Created: ${realm.createdAt.toLocaleDateString()}`); console.log(`Last seen: ${realm.lastSeen.toLocaleDateString()}`); const lores = db.listLoresByRealm(realm.id); console.log(`\nLores: ${lores.length}`); const loresByType = lores.reduce((acc, lore) => { acc[lore.type] = (acc[lore.type] || 0) + 1; return acc; }, {}); Object.entries(loresByType).forEach(([type, count]) => { console.log(` ${type}: ${count}`); }); // Show embedding info const embeddingService = EmbeddingService.getInstance(); const modelConfig = embeddingService.getModelConfig(); console.log(`\nEmbedding Configuration:`); console.log(`Model: ${embeddingService.getCurrentModel()}`); console.log(`Dimensions: ${modelConfig.dimensions}`); } else { console.log('No LoreHub realm found in this directory.'); console.log('Run "lh add" to create your first lore.'); } db.close(); } else { // Interactive mode with Ink await renderRealmInfo(); } } catch (error) { console.error('Error:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); // Export command program .command('export <output>') .description('Export lores to a file') .option('-r, --realm <path>', 'Export lores from specific realm only') .option('-f, --format <format>', 'Output format (json, markdown)', 'json') .action(async (output, options) => { try { await renderExport({ realmPath: options.realm, outputFile: output, format: options.format, }); } catch (error) { console.error('Error:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); // Import command program .command('import <input>') .description('Import lores from a file') .option('-m, --merge', 'Merge with existing data (default: replace)') .action(async (input, options) => { try { await renderImport({ inputFile: input, merge: options.merge || false, }); } catch (error) { console.error('Error:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); // Similar command program .command('similar <loreId>') .description('Find lores similar to a given lore (kindred lores)') .option('-l, --limit <number>', 'Maximum number of kindred lores to show', '10') .option('-t, --threshold <number>', 'Similarity threshold (0-1)', '0.5') .action(async (loreId, options) => { try { const dbPath = getDbPath(); const db = new Database(dbPath); const lore = db.findLore(loreId); if (!lore) { console.error(`Lore with ID ${loreId} not found`); process.exit(1); } console.log(`\nFinding lores similar to:\n[${lore.type}] ${lore.content}\n`); const similarLores = await db.findSimilarLores(loreId, { limit: parseInt(options.limit), threshold: parseFloat(options.threshold) }); if (similarLores.length === 0) { console.log('No kindred lores found'); } else { console.log(`Found ${similarLores.length} kindred lore${similarLores.length === 1 ? '' : 's'}:\n`); similarLores.forEach((similar, index) => { const realm = db.findRealm(similar.realmId || similar.realmId); console.log(`${index + 1}. [${similar.type}] ${similar.content}`); console.log(` Similarity: ${(similar.similarity * 100).toFixed(1)}%`); console.log(` Realm: ${realm?.name || 'Unknown'} (${realm?.path || 'Unknown'})`); if (similar.why) { console.log(` Why: ${similar.why}`); } console.log(''); }); } db.close(); } catch (error) { console.error('Error:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); // Migrate embeddings command program.addCommand(migrateEmbeddingsCommand); // Config command program.addCommand(configCommand); // Workspace command program.addCommand(workspaceCommand); // Sync command program.addCommand(syncCommand); return program; } export async function run(argv) { const program = createCLI(); const args = argv || process.argv; // If no arguments provided, default to browse command if (args.length === 2) { // argv is like ['node', 'script.js'] when no args await renderBrowse({}); return; } program.parse(args); } //# sourceMappingURL=cli.js.map