import { Command } from 'commander'; import ora from 'ora'; import { IndemnClient } from '../sdk/client.js'; import { KBSDK } from '../sdk/kb.js'; import { formatTable, printJson, printError, printSuccess, confirm } from './utils/output.js'; export function registerKBCommands(program: Command): void { const kb = program.command('kb').description('Manage knowledge bases'); kb.command('list') .description('List all knowledge bases') .option('--json', 'Output as JSON') .option('--limit ', 'Limit number of results') .option('--page ', 'Page number (use with --limit)') .action(async (options) => { try { const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Fetching knowledge bases...').start(); let kbs = await sdk.listKBs(); spinner.stop(); if (options.page && options.limit) { const page = parseInt(options.page, 10); const limit = parseInt(options.limit, 10); kbs = kbs.slice((page - 1) * limit, page * limit); } else if (options.limit) { kbs = kbs.slice(0, parseInt(options.limit, 10)); } if (options.json) { printJson(kbs); return; } if (kbs.length === 0) { console.log('No knowledge bases found.'); return; } const rows = kbs.map((k) => [ k._id, k.name || '', k.type || '', ]); console.log(formatTable(['ID', 'Name', 'Type'], rows)); } catch (err) { printError(err); process.exit(1); } }); kb.command('get') .argument('', 'Knowledge base ID') .description('Get knowledge base details') .option('--json', 'Output as JSON') .action(async (kbId: string, options) => { try { const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Fetching knowledge base...').start(); const knowledgeBase = await sdk.getKB(kbId); spinner.stop(); if (options.json) { printJson(knowledgeBase); return; } console.log(`ID: ${knowledgeBase._id}`); console.log(`Name: ${knowledgeBase.name}`); console.log(`Type: ${knowledgeBase.type || '(none)'}`); console.log(`Description: ${knowledgeBase.description || '(none)'}`); console.log(`Project: ${knowledgeBase.id_project || '(none)'}`); console.log(`Created: ${knowledgeBase.createdAt || ''}`); console.log(`Updated: ${knowledgeBase.updatedAt || ''}`); } catch (err) { printError(err); process.exit(1); } }); kb.command('create') .description('Create a new knowledge base') .requiredOption('--name ', 'Knowledge base name') .option('--type ', 'Knowledge base type (QNA, URL, FILE)', 'QNA') .action(async (options) => { try { const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Creating knowledge base...').start(); const knowledgeBase = await sdk.createKB({ name: options.name, type: options.type.toUpperCase(), }); spinner.stop(); printSuccess(`Knowledge base created: ${knowledgeBase._id}`); console.log(` Name: ${knowledgeBase.name}`); console.log(` Type: ${knowledgeBase.type || options.type}`); } catch (err) { printError(err); process.exit(1); } }); kb.command('update') .argument('', 'Knowledge base ID') .description('Update a knowledge base') .option('--name ', 'New name') .action(async (kbId: string, options) => { try { const input: Record = {}; if (options.name) input.name = options.name; if (Object.keys(input).length === 0) { printError('No update options provided. Use --name.'); process.exit(1); } const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Updating knowledge base...').start(); await sdk.updateKB(kbId, input); spinner.stop(); printSuccess(`Knowledge base ${kbId} updated.`); } catch (err) { printError(err); process.exit(1); } }); kb.command('delete') .argument('', 'Knowledge base ID') .description('Delete a knowledge base') .action(async (kbId: string) => { try { const confirmed = await confirm(`Delete knowledge base ${kbId}? This cannot be undone.`); if (!confirmed) { console.log('Cancelled.'); return; } const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Deleting knowledge base...').start(); await sdk.deleteKB(kbId); spinner.stop(); printSuccess(`Knowledge base ${kbId} deleted.`); } catch (err) { printError(err); process.exit(1); } }); // Data source subcommands const data = kb.command('data').description('Manage KB data sources'); data.command('list') .argument('', 'Knowledge base ID') .description('List data sources') .option('--json', 'Output as JSON') .option('--limit ', 'Limit number of results') .option('--page ', 'Page number') .action(async (kbId: string, options) => { try { const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Fetching data sources...').start(); const query: Record = {}; if (options.limit) query.limit = options.limit; if (options.page) query.page = options.page; const result = await sdk.listDataSources(kbId, query); spinner.stop(); const sources = result.dataSources || []; if (options.json) { printJson(result); return; } if (sources.length === 0) { console.log('No data sources found.'); return; } console.log(`Total: ${result.total} | Page ${result.page} of ${result.pages}`); const rows = sources.map((s) => [ s._id, s.name || s.question || '', s.type || '', s.status || '', ]); console.log(formatTable(['ID', 'Name/Question', 'Type', 'Status'], rows)); } catch (err) { printError(err); process.exit(1); } }); data.command('add') .argument('', 'Knowledge base ID') .description('Add a data source') .option('--question ', 'Question text (for QnA)') .option('--answer ', 'Answer text (for QnA)') .option('--source-url ', 'Source URL') .option('--content ', 'Content text') .option('--file ', 'File to upload (for file-type KBs)') .action(async (kbId: string, options) => { try { const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Adding data source...').start(); if (options.sourceUrl) { const result = await sdk.scrapeUrl(kbId, options.sourceUrl); spinner.stop(); printSuccess(`URL scraping initiated for: ${options.sourceUrl}`); printJson(result); } else if (options.file) { const result = await sdk.uploadFile(kbId, options.file); spinner.stop(); printSuccess(`File uploaded: ${options.file}`); printJson(result); } else if (options.question && options.answer) { const source = await sdk.addDataSource(kbId, { question: options.question, answer: options.answer }); spinner.stop(); printSuccess(`Data source added: ${source._id}`); } else { spinner.stop(); printError('Provide --question/--answer, --source-url, or --file.'); process.exit(1); } } catch (err) { printError(err); process.exit(1); } }); data.command('update') .argument('', 'Knowledge base ID') .argument('', 'Data source ID') .description('Update a data source') .option('--question ', 'Question text') .option('--answer ', 'Answer text') .option('--source-url ', 'Source URL') .option('--content ', 'Content text') .action(async (kbId: string, sourceId: string, options) => { try { const input: Record = {}; if (options.question) input.question = options.question; if (options.answer) input.answer = options.answer; if (options.sourceUrl) input.source_url = options.sourceUrl; if (options.content) input.content = options.content; if (Object.keys(input).length === 0) { printError('No update options provided.'); process.exit(1); } const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Updating data source...').start(); await sdk.updateDataSource(kbId, sourceId, input); spinner.stop(); printSuccess(`Data source ${sourceId} updated.`); } catch (err) { printError(err); process.exit(1); } }); data.command('delete') .argument('', 'Knowledge base ID') .argument('', 'Data source ID') .description('Delete a data source') .action(async (kbId: string, sourceId: string) => { try { const confirmed = await confirm(`Delete data source ${sourceId}? This cannot be undone.`); if (!confirmed) { console.log('Cancelled.'); return; } const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Deleting data source...').start(); await sdk.deleteDataSource(kbId, sourceId); spinner.stop(); printSuccess(`Data source ${sourceId} deleted.`); } catch (err) { printError(err); process.exit(1); } }); // Link/unlink commands kb.command('link') .argument('', 'Agent ID') .argument('', 'Knowledge base IDs') .description('Link knowledge bases to an agent') .action(async (agentId: string, kbIds: string[]) => { try { const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Linking knowledge bases...').start(); await sdk.linkKB(agentId, kbIds); spinner.stop(); printSuccess(`Linked ${kbIds.length} knowledge base(s) to agent ${agentId}.`); } catch (err) { printError(err); process.exit(1); } }); kb.command('unlink') .argument('', 'Agent ID') .argument('', 'Knowledge base ID') .description('Unlink a knowledge base from an agent') .action(async (agentId: string, kbId: string) => { try { const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Unlinking knowledge base...').start(); await sdk.unlinkKB(agentId, kbId); spinner.stop(); printSuccess(`Unlinked knowledge base ${kbId} from agent ${agentId}.`); } catch (err) { printError(err); process.exit(1); } }); kb.command('export') .argument('', 'Knowledge base ID') .description('Export knowledge base data') .option('--format ', 'Export format (csv or xlsx)', 'csv') .action(async (kbId: string, options) => { try { const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora('Exporting knowledge base...').start(); const result = await sdk.exportKB(kbId, options.format); spinner.stop(); printJson(result); } catch (err) { printError(err); process.exit(1); } }); kb.command('import') .argument('', 'Knowledge base ID') .description('Import QNA pairs from a JSON file into a knowledge base') .requiredOption('--file ', 'Path to JSON file with array of {question, answer} pairs') .action(async (kbId: string, options) => { try { const fs = await import('fs'); const filePath = options.file as string; if (!fs.existsSync(filePath)) { printError(`File not found: ${filePath}`); process.exit(1); } const content = fs.readFileSync(filePath, 'utf-8'); let data: unknown; try { data = JSON.parse(content); } catch { printError('Import file must be valid JSON.'); process.exit(1); } if (!Array.isArray(data)) { printError('Import file must contain a JSON array of {question, answer} objects.'); process.exit(1); } const pairs = data as Array<{ question?: string; answer?: string }>; const invalid = pairs.filter(p => !p.question || !p.answer); if (invalid.length > 0) { printError(`${invalid.length} item(s) missing "question" or "answer" field.`); process.exit(1); } const client = new IndemnClient(); const sdk = new KBSDK(client); const spinner = ora(`Importing ${pairs.length} QNA pairs...`).start(); let added = 0; let failed = 0; for (const pair of pairs) { try { await sdk.addDataSource(kbId, { question: pair.question!, answer: pair.answer! }); added++; spinner.text = `Importing QNA pairs... ${added}/${pairs.length}`; } catch { failed++; } } spinner.stop(); printSuccess(`Imported ${added}/${pairs.length} QNA pairs.${failed > 0 ? ` ${failed} failed.` : ''}`); } catch (err) { printError(err); process.exit(1); } }); }