@kiyeonjeon21/datacontext 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.cursorrules +12 -0
  2. package/.env.example +8 -0
  3. package/.github/workflows/ci.yml +21 -1
  4. package/.github/workflows/publish.yml +21 -1
  5. package/CHANGELOG.md +41 -0
  6. package/README.md +247 -239
  7. package/datacontext.db +0 -0
  8. package/dist/api/server.d.ts.map +1 -1
  9. package/dist/api/server.js +145 -0
  10. package/dist/api/server.js.map +1 -1
  11. package/dist/api/start-server.d.ts +10 -0
  12. package/dist/api/start-server.d.ts.map +1 -0
  13. package/dist/api/start-server.js +73 -0
  14. package/dist/api/start-server.js.map +1 -0
  15. package/dist/cli/index.js +462 -0
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/core/context-service.d.ts +58 -0
  18. package/dist/core/context-service.d.ts.map +1 -1
  19. package/dist/core/context-service.js +121 -0
  20. package/dist/core/context-service.js.map +1 -1
  21. package/dist/core/index.d.ts +2 -0
  22. package/dist/core/index.d.ts.map +1 -1
  23. package/dist/core/index.js +5 -1
  24. package/dist/core/index.js.map +1 -1
  25. package/dist/core/llm-service.d.ts +141 -0
  26. package/dist/core/llm-service.d.ts.map +1 -0
  27. package/dist/core/llm-service.js +284 -0
  28. package/dist/core/llm-service.js.map +1 -0
  29. package/dist/knowledge/store.d.ts +56 -3
  30. package/dist/knowledge/store.d.ts.map +1 -1
  31. package/dist/knowledge/store.js +193 -7
  32. package/dist/knowledge/store.js.map +1 -1
  33. package/dist/knowledge/types.d.ts +43 -1
  34. package/dist/knowledge/types.d.ts.map +1 -1
  35. package/dist/knowledge/types.js.map +1 -1
  36. package/dist/mcp/tools.d.ts.map +1 -1
  37. package/dist/mcp/tools.js +365 -0
  38. package/dist/mcp/tools.js.map +1 -1
  39. package/docs/API.md +173 -0
  40. package/docs/DEMO_SCRIPT.md +210 -0
  41. package/docs/SYNC_GUIDE.md +242 -0
  42. package/package.json +4 -1
  43. package/src/api/server.ts +160 -0
  44. package/src/api/start-server.ts +78 -0
  45. package/src/cli/index.ts +534 -0
  46. package/src/core/context-service.ts +157 -0
  47. package/src/core/index.ts +7 -0
  48. package/src/core/llm-service.ts +359 -0
  49. package/src/knowledge/store.ts +232 -7
  50. package/src/knowledge/types.ts +45 -1
  51. package/src/mcp/tools.ts +415 -0
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Start DataContext REST API Server
4
+ *
5
+ * Usage:
6
+ * node dist/api/start-server.js
7
+ * DATABASE_URL=sqlite:./test.db npm run serve
8
+ */
9
+
10
+ import 'dotenv/config';
11
+ import { createPostgresAdapter } from '../adapters/postgres.js';
12
+ import { createSQLiteAdapter } from '../adapters/sqlite.js';
13
+ import { createDataContextService } from '../core/context-service.js';
14
+ import { KnowledgeStore } from '../knowledge/store.js';
15
+ import { startApiServer } from './server.js';
16
+
17
+ async function main() {
18
+ const port = parseInt(process.env.PORT || '3000', 10);
19
+ const dbUrl = process.env.DATABASE_URL || 'sqlite:./datacontext.db';
20
+
21
+ console.log('[datacontext-api] Starting server...');
22
+ console.log(`[datacontext-api] Port: ${port}`);
23
+ console.log(`[datacontext-api] Database: ${dbUrl.split('?')[0]}`);
24
+
25
+ // Create appropriate adapter based on URL
26
+ let adapter;
27
+ if (dbUrl.startsWith('postgres://') || dbUrl.startsWith('postgresql://')) {
28
+ adapter = createPostgresAdapter(dbUrl);
29
+ } else if (dbUrl.startsWith('sqlite:')) {
30
+ const dbPath = dbUrl.replace('sqlite:', '');
31
+ adapter = createSQLiteAdapter(dbPath);
32
+ } else {
33
+ console.error('[datacontext-api] Unsupported database URL. Use postgres:// or sqlite:');
34
+ process.exit(1);
35
+ }
36
+
37
+ // Create knowledge store
38
+ const databaseId = dbUrl.split('/').pop()?.split('?')[0] || 'default';
39
+ const knowledge = new KnowledgeStore(databaseId);
40
+
41
+ // Create service
42
+ const service = createDataContextService({
43
+ adapter,
44
+ knowledge,
45
+ safety: {
46
+ readOnly: true,
47
+ timeoutMs: 30000,
48
+ maxRows: 1000,
49
+ },
50
+ });
51
+
52
+ // Initialize service
53
+ await service.initialize();
54
+ console.log('[datacontext-api] ✓ Service initialized');
55
+
56
+ try {
57
+ await startApiServer({
58
+ service,
59
+ port,
60
+ cors: true,
61
+ });
62
+
63
+ console.log('[datacontext-api] ✓ Server started successfully');
64
+ console.log('[datacontext-api] ✓ API: http://localhost:' + port + '/api');
65
+ console.log('[datacontext-api] ✓ Health: http://localhost:' + port + '/health');
66
+ console.log('');
67
+ console.log('[datacontext-api] Press Ctrl+C to stop');
68
+ } catch (error) {
69
+ console.error('[datacontext-api] Failed to start server:', error);
70
+ process.exit(1);
71
+ }
72
+ }
73
+
74
+ main().catch((error) => {
75
+ console.error('[datacontext-api] Fatal error:', error);
76
+ process.exit(1);
77
+ });
78
+
package/src/cli/index.ts CHANGED
@@ -282,6 +282,540 @@ function printTable(table: { name: string; schema: string; type: string; columns
282
282
  }
283
283
  }
284
284
 
285
+ // ============================================================
286
+ // Glossary Commands
287
+ // ============================================================
288
+
289
+ const glossaryCmd = program
290
+ .command('glossary')
291
+ .description('Manage business glossary/terms');
292
+
293
+ /**
294
+ * List all terms
295
+ */
296
+ glossaryCmd
297
+ .command('list')
298
+ .description('List all business terms in the glossary')
299
+ .option('--connection <string>', 'Database connection string')
300
+ .option('--category <cat>', 'Filter by category (status, time, money, entity, metric, filter, custom)')
301
+ .option('--table <table>', 'Filter by table name')
302
+ .option('--json', 'Output as JSON')
303
+ .action(async (options) => {
304
+ try {
305
+ const knowledge = await loadKnowledgeStore(options.connection);
306
+ let terms = knowledge.getActiveTerms();
307
+
308
+ // Apply filters
309
+ if (options.category) {
310
+ terms = terms.filter(t => t.category === options.category);
311
+ }
312
+ if (options.table) {
313
+ terms = terms.filter(t => t.appliesTo?.tables?.includes(options.table));
314
+ }
315
+
316
+ if (options.json) {
317
+ console.log(JSON.stringify(terms, null, 2));
318
+ } else {
319
+ if (terms.length === 0) {
320
+ console.log('[datacontext] No terms found. Use "glossary add" or "glossary generate" to add terms.');
321
+ return;
322
+ }
323
+
324
+ console.log(`\n📚 Business Glossary (${terms.length} terms)\n`);
325
+ for (const term of terms) {
326
+ console.log(` ${term.term}`);
327
+ console.log(` Definition: ${term.definition}`);
328
+ if (term.sqlExpression) {
329
+ console.log(` SQL: ${term.sqlExpression}`);
330
+ }
331
+ if (term.synonyms.length > 0) {
332
+ console.log(` Synonyms: ${term.synonyms.join(', ')}`);
333
+ }
334
+ if (term.appliesTo?.tables?.length) {
335
+ console.log(` Tables: ${term.appliesTo.tables.join(', ')}`);
336
+ }
337
+ if (term.category) {
338
+ console.log(` Category: ${term.category}`);
339
+ }
340
+ console.log('');
341
+ }
342
+ }
343
+ } catch (error) {
344
+ console.error(`[datacontext] Error: ${error instanceof Error ? error.message : error}`);
345
+ process.exit(1);
346
+ }
347
+ });
348
+
349
+ /**
350
+ * Add a term manually
351
+ */
352
+ glossaryCmd
353
+ .command('add')
354
+ .description('Add a business term to the glossary')
355
+ .argument('<term>', 'The business term (e.g., "활성 사용자")')
356
+ .argument('<definition>', 'Definition of the term')
357
+ .option('--sql <expression>', 'SQL expression (e.g., "status = 1")')
358
+ .option('--synonyms <list>', 'Comma-separated synonyms')
359
+ .option('--tables <list>', 'Comma-separated table names')
360
+ .option('--category <cat>', 'Category: status, time, money, entity, metric, filter, custom')
361
+ .option('--connection <string>', 'Database connection string')
362
+ .action(async (term: string, definition: string, options) => {
363
+ try {
364
+ const knowledge = await loadKnowledgeStore(options.connection);
365
+
366
+ const added = await knowledge.addBusinessTerm(term, definition, {
367
+ sqlExpression: options.sql,
368
+ synonyms: options.synonyms ? options.synonyms.split(',').map((s: string) => s.trim()) : undefined,
369
+ appliesTo: options.tables ? { tables: options.tables.split(',').map((s: string) => s.trim()) } : undefined,
370
+ category: options.category,
371
+ });
372
+
373
+ console.log(`[datacontext] ✓ Added term: "${added.term}"`);
374
+ if (added.sqlExpression) {
375
+ console.log(`[datacontext] SQL: ${added.sqlExpression}`);
376
+ }
377
+ } catch (error) {
378
+ console.error(`[datacontext] Error: ${error instanceof Error ? error.message : error}`);
379
+ process.exit(1);
380
+ }
381
+ });
382
+
383
+ /**
384
+ * Search terms
385
+ */
386
+ glossaryCmd
387
+ .command('search')
388
+ .description('Search for business terms')
389
+ .argument('<query>', 'Search query')
390
+ .option('--connection <string>', 'Database connection string')
391
+ .option('--json', 'Output as JSON')
392
+ .action(async (query: string, options) => {
393
+ try {
394
+ const knowledge = await loadKnowledgeStore(options.connection);
395
+ const terms = knowledge.findMatchingTerms(query);
396
+
397
+ if (options.json) {
398
+ console.log(JSON.stringify({
399
+ query,
400
+ count: terms.length,
401
+ terms,
402
+ suggestedConditions: terms.filter(t => t.sqlExpression).map(t => t.sqlExpression),
403
+ }, null, 2));
404
+ } else {
405
+ if (terms.length === 0) {
406
+ console.log(`[datacontext] No terms found matching "${query}"`);
407
+ return;
408
+ }
409
+
410
+ console.log(`\n🔍 Search results for "${query}" (${terms.length} matches)\n`);
411
+ for (const term of terms) {
412
+ console.log(` ${term.term}`);
413
+ console.log(` ${term.definition}`);
414
+ if (term.sqlExpression) {
415
+ console.log(` → SQL: ${term.sqlExpression}`);
416
+ }
417
+ console.log('');
418
+ }
419
+
420
+ const conditions = terms.filter(t => t.sqlExpression).map(t => t.sqlExpression);
421
+ if (conditions.length > 0) {
422
+ console.log(' 💡 Suggested SQL conditions:');
423
+ for (const cond of conditions) {
424
+ console.log(` ${cond}`);
425
+ }
426
+ }
427
+ }
428
+ } catch (error) {
429
+ console.error(`[datacontext] Error: ${error instanceof Error ? error.message : error}`);
430
+ process.exit(1);
431
+ }
432
+ });
433
+
434
+ /**
435
+ * Generate terms using AI
436
+ */
437
+ glossaryCmd
438
+ .command('generate')
439
+ .description('Generate business terms from natural language using AI')
440
+ .argument('<terms>', 'Raw term definitions (e.g., "활성 사용자 = status가 1인 사용자, 최근 주문 = 30일 이내")')
441
+ .option('--connection <string>', 'Database connection string')
442
+ .option('--json', 'Output as JSON')
443
+ .action(async (rawTerms: string, options) => {
444
+ try {
445
+ // Load .env for API key
446
+ await import('dotenv/config');
447
+
448
+ const { isLLMAvailable, createLLMService } = await import('../core/llm-service.js');
449
+
450
+ if (!isLLMAvailable()) {
451
+ console.error('[datacontext] Error: ANTHROPIC_API_KEY not configured.');
452
+ console.error('[datacontext] Set the environment variable or add it to .env file.');
453
+ process.exit(1);
454
+ }
455
+
456
+ const knowledge = await loadKnowledgeStore(options.connection);
457
+
458
+ console.log('[datacontext] Generating terms using AI...');
459
+
460
+ // Get schema context if connected to DB
461
+ type SchemaContextType = import('../core/llm-service.js').SchemaContext;
462
+ let schemaContext: SchemaContextType = {
463
+ tables: [] as SchemaContextType['tables'],
464
+ existingTerms: knowledge.getBusinessTerms()
465
+ };
466
+
467
+ if (options.connection) {
468
+ const adapter = createAdapter(options.connection);
469
+ await adapter.connect();
470
+ const schemaInfo = await adapter.getSchema();
471
+ schemaContext = {
472
+ tables: schemaInfo.tables.slice(0, 20).map(t => ({
473
+ name: t.name,
474
+ columns: t.columns.map(c => ({
475
+ name: c.name,
476
+ type: c.dataType,
477
+ nullable: c.isNullable,
478
+ })),
479
+ })),
480
+ existingTerms: knowledge.getBusinessTerms(),
481
+ };
482
+ await adapter.disconnect();
483
+ }
484
+
485
+ const llm = createLLMService();
486
+ const generated = await llm.generateGlossary(
487
+ rawTerms,
488
+ schemaContext,
489
+ knowledge.getSchemaHash()
490
+ );
491
+
492
+ // Save to knowledge store
493
+ const added = await knowledge.addBusinessTerms(generated);
494
+
495
+ if (options.json) {
496
+ console.log(JSON.stringify({
497
+ success: true,
498
+ generated: added.length,
499
+ terms: added,
500
+ }, null, 2));
501
+ } else {
502
+ console.log(`\n[datacontext] ✓ Generated ${added.length} term(s)\n`);
503
+ for (const term of added) {
504
+ console.log(` 📝 ${term.term}`);
505
+ console.log(` Definition: ${term.definition}`);
506
+ if (term.sqlExpression) {
507
+ console.log(` SQL: ${term.sqlExpression}`);
508
+ }
509
+ if (term.synonyms.length > 0) {
510
+ console.log(` Synonyms: ${term.synonyms.join(', ')}`);
511
+ }
512
+ console.log('');
513
+ }
514
+ }
515
+ } catch (error) {
516
+ console.error(`[datacontext] Error: ${error instanceof Error ? error.message : error}`);
517
+ process.exit(1);
518
+ }
519
+ });
520
+
521
+ /**
522
+ * Export glossary to YAML/JSON
523
+ */
524
+ glossaryCmd
525
+ .command('export')
526
+ .description('Export glossary to YAML or JSON file')
527
+ .option('--connection <string>', 'Database connection string')
528
+ .option('--format <format>', 'Output format: yaml or json', 'yaml')
529
+ .option('--output <file>', 'Output file path (default: stdout)')
530
+ .action(async (options) => {
531
+ try {
532
+ const knowledge = await loadKnowledgeStore(options.connection);
533
+ const terms = knowledge.getActiveTerms();
534
+
535
+ if (terms.length === 0) {
536
+ console.error('[datacontext] No terms to export.');
537
+ process.exit(1);
538
+ }
539
+
540
+ const exportData = {
541
+ version: '1.0.0',
542
+ exportedAt: new Date().toISOString(),
543
+ terms: terms.map(t => ({
544
+ term: t.term,
545
+ definition: t.definition,
546
+ sql: t.sqlExpression,
547
+ synonyms: t.synonyms,
548
+ tables: t.appliesTo?.tables,
549
+ columns: t.appliesTo?.columns,
550
+ category: t.category,
551
+ examples: t.examples,
552
+ })),
553
+ };
554
+
555
+ let output: string;
556
+ if (options.format === 'yaml') {
557
+ // Simple YAML serialization
558
+ output = serializeToYaml(exportData);
559
+ } else {
560
+ output = JSON.stringify(exportData, null, 2);
561
+ }
562
+
563
+ if (options.output) {
564
+ const fs = await import('fs/promises');
565
+ await fs.writeFile(options.output, output);
566
+ console.log(`[datacontext] ✓ Exported ${terms.length} terms to ${options.output}`);
567
+ } else {
568
+ console.log(output);
569
+ }
570
+ } catch (error) {
571
+ console.error(`[datacontext] Error: ${error instanceof Error ? error.message : error}`);
572
+ process.exit(1);
573
+ }
574
+ });
575
+
576
+ /**
577
+ * Import glossary from YAML/JSON
578
+ */
579
+ glossaryCmd
580
+ .command('import')
581
+ .description('Import glossary from YAML or JSON file')
582
+ .argument('<file>', 'Input file path')
583
+ .option('--connection <string>', 'Database connection string')
584
+ .option('--merge', 'Merge with existing terms (default: true)', true)
585
+ .option('--replace', 'Replace all existing terms')
586
+ .action(async (file: string, options) => {
587
+ try {
588
+ const fs = await import('fs/promises');
589
+ const content = await fs.readFile(file, 'utf-8');
590
+
591
+ let data: { terms: Array<{
592
+ term: string;
593
+ definition: string;
594
+ sql?: string;
595
+ synonyms?: string[];
596
+ tables?: string[];
597
+ columns?: string[];
598
+ category?: string;
599
+ examples?: string[];
600
+ }> };
601
+
602
+ if (file.endsWith('.yaml') || file.endsWith('.yml')) {
603
+ data = parseYaml(content);
604
+ } else {
605
+ data = JSON.parse(content);
606
+ }
607
+
608
+ if (!data.terms || !Array.isArray(data.terms)) {
609
+ console.error('[datacontext] Error: Invalid file format. Expected "terms" array.');
610
+ process.exit(1);
611
+ }
612
+
613
+ const knowledge = await loadKnowledgeStore(options.connection);
614
+
615
+ let added = 0;
616
+ for (const term of data.terms) {
617
+ await knowledge.addBusinessTerm(term.term, term.definition, {
618
+ sqlExpression: term.sql,
619
+ synonyms: term.synonyms,
620
+ appliesTo: term.tables || term.columns ? {
621
+ tables: term.tables,
622
+ columns: term.columns,
623
+ } : undefined,
624
+ category: term.category as import('../knowledge/types.js').TermCategory,
625
+ examples: term.examples,
626
+ });
627
+ added++;
628
+ }
629
+
630
+ console.log(`[datacontext] ✓ Imported ${added} term(s) from ${file}`);
631
+ } catch (error) {
632
+ console.error(`[datacontext] Error: ${error instanceof Error ? error.message : error}`);
633
+ process.exit(1);
634
+ }
635
+ });
636
+
637
+ /**
638
+ * Delete a term
639
+ */
640
+ glossaryCmd
641
+ .command('delete')
642
+ .description('Delete a business term')
643
+ .argument('<term>', 'Term name or ID to delete')
644
+ .option('--connection <string>', 'Database connection string')
645
+ .action(async (termToDelete: string, options) => {
646
+ try {
647
+ const knowledge = await loadKnowledgeStore(options.connection);
648
+ const terms = knowledge.getBusinessTerms();
649
+
650
+ // Find by term name or ID
651
+ const found = terms.find(t => t.term === termToDelete || t.id === termToDelete);
652
+ if (!found) {
653
+ console.error(`[datacontext] Error: Term "${termToDelete}" not found.`);
654
+ process.exit(1);
655
+ }
656
+
657
+ await knowledge.deleteBusinessTerm(found.id);
658
+ console.log(`[datacontext] ✓ Deleted term: "${found.term}"`);
659
+ } catch (error) {
660
+ console.error(`[datacontext] Error: ${error instanceof Error ? error.message : error}`);
661
+ process.exit(1);
662
+ }
663
+ });
664
+
665
+ // ============================================================
666
+ // Helper Functions
667
+ // ============================================================
668
+
669
+ /**
670
+ * Load knowledge store with optional connection
671
+ */
672
+ async function loadKnowledgeStore(connectionString?: string): Promise<KnowledgeStore> {
673
+ let databaseId = 'default';
674
+
675
+ if (connectionString) {
676
+ const config = parseConnectionString(connectionString);
677
+ databaseId = `${config.host}_${config.database}`;
678
+ }
679
+
680
+ const knowledge = createKnowledgeStore(databaseId);
681
+ await knowledge.load();
682
+ return knowledge;
683
+ }
684
+
685
+ /**
686
+ * Simple YAML serializer for export
687
+ */
688
+ function serializeToYaml(data: unknown): string {
689
+ const lines: string[] = [];
690
+
691
+ function serialize(obj: unknown, indent: number = 0): void {
692
+ const prefix = ' '.repeat(indent);
693
+
694
+ if (Array.isArray(obj)) {
695
+ for (const item of obj) {
696
+ if (typeof item === 'object' && item !== null) {
697
+ lines.push(`${prefix}-`);
698
+ serialize(item, indent + 1);
699
+ } else {
700
+ lines.push(`${prefix}- ${formatValue(item)}`);
701
+ }
702
+ }
703
+ } else if (typeof obj === 'object' && obj !== null) {
704
+ for (const [key, value] of Object.entries(obj)) {
705
+ if (value === undefined || value === null) continue;
706
+
707
+ if (Array.isArray(value)) {
708
+ if (value.length === 0) continue;
709
+ if (typeof value[0] === 'string') {
710
+ lines.push(`${prefix}${key}: [${value.map(v => `"${v}"`).join(', ')}]`);
711
+ } else {
712
+ lines.push(`${prefix}${key}:`);
713
+ serialize(value, indent + 1);
714
+ }
715
+ } else if (typeof value === 'object') {
716
+ lines.push(`${prefix}${key}:`);
717
+ serialize(value, indent + 1);
718
+ } else {
719
+ lines.push(`${prefix}${key}: ${formatValue(value)}`);
720
+ }
721
+ }
722
+ }
723
+ }
724
+
725
+ function formatValue(val: unknown): string {
726
+ if (typeof val === 'string') {
727
+ if (val.includes('\n') || val.includes(':') || val.includes('#')) {
728
+ return `"${val.replace(/"/g, '\\"')}"`;
729
+ }
730
+ return val;
731
+ }
732
+ return String(val);
733
+ }
734
+
735
+ serialize(data);
736
+ return lines.join('\n');
737
+ }
738
+
739
+ /**
740
+ * Simple YAML parser for import
741
+ */
742
+ function parseYaml(content: string): { terms: Array<{
743
+ term: string;
744
+ definition: string;
745
+ sql?: string;
746
+ synonyms?: string[];
747
+ tables?: string[];
748
+ columns?: string[];
749
+ category?: string;
750
+ examples?: string[];
751
+ }> } {
752
+ const lines = content.split('\n');
753
+ const result: { terms: unknown[] } = { terms: [] };
754
+ let currentTerm: Record<string, unknown> | null = null;
755
+ let inTermsArray = false;
756
+
757
+ for (const rawLine of lines) {
758
+ const line = rawLine.trimEnd();
759
+ if (!line || line.startsWith('#')) continue;
760
+
761
+ // Check for terms array start
762
+ if (line.match(/^terms:\s*$/)) {
763
+ inTermsArray = true;
764
+ continue;
765
+ }
766
+
767
+ if (!inTermsArray) continue;
768
+
769
+ // New term
770
+ if (line.match(/^\s*-\s*$/)) {
771
+ if (currentTerm) {
772
+ result.terms.push(currentTerm);
773
+ }
774
+ currentTerm = {};
775
+ continue;
776
+ }
777
+
778
+ // Property
779
+ const match = line.match(/^\s+(\w+):\s*(.*)$/);
780
+ if (match && currentTerm) {
781
+ const [, key, rawValue] = match;
782
+ let value: unknown = rawValue.trim();
783
+
784
+ // Parse array values
785
+ if (typeof value === 'string' && value.startsWith('[') && value.endsWith(']')) {
786
+ value = value.slice(1, -1).split(',').map(s =>
787
+ s.trim().replace(/^["']|["']$/g, '')
788
+ );
789
+ }
790
+ // Remove quotes
791
+ if (typeof value === 'string' && (value.startsWith('"') || value.startsWith("'"))) {
792
+ value = value.slice(1, -1);
793
+ }
794
+
795
+ currentTerm[key] = value;
796
+ }
797
+ }
798
+
799
+ if (currentTerm) {
800
+ result.terms.push(currentTerm);
801
+ }
802
+
803
+ return result as { terms: Array<{
804
+ term: string;
805
+ definition: string;
806
+ sql?: string;
807
+ synonyms?: string[];
808
+ tables?: string[];
809
+ columns?: string[];
810
+ category?: string;
811
+ examples?: string[];
812
+ }> };
813
+ }
814
+
815
+ // ============================================================
816
+ // Server Commands
817
+ // ============================================================
818
+
285
819
  /**
286
820
  * Serve command - Start REST API server
287
821
  */