@kiyeonjeon21/datacontext 0.2.0 → 0.3.1

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 (55) 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/cursor-mcp-config.json.example +29 -0
  8. package/datacontext.db +0 -0
  9. package/dist/api/server.d.ts.map +1 -1
  10. package/dist/api/server.js +145 -0
  11. package/dist/api/server.js.map +1 -1
  12. package/dist/api/start-server.d.ts +10 -0
  13. package/dist/api/start-server.d.ts.map +1 -0
  14. package/dist/api/start-server.js +73 -0
  15. package/dist/api/start-server.js.map +1 -0
  16. package/dist/cli/index.js +462 -0
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/core/context-service.d.ts +72 -0
  19. package/dist/core/context-service.d.ts.map +1 -1
  20. package/dist/core/context-service.js +132 -0
  21. package/dist/core/context-service.js.map +1 -1
  22. package/dist/core/index.d.ts +2 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/index.js +5 -1
  25. package/dist/core/index.js.map +1 -1
  26. package/dist/core/llm-service.d.ts +141 -0
  27. package/dist/core/llm-service.d.ts.map +1 -0
  28. package/dist/core/llm-service.js +284 -0
  29. package/dist/core/llm-service.js.map +1 -0
  30. package/dist/knowledge/store.d.ts +56 -3
  31. package/dist/knowledge/store.d.ts.map +1 -1
  32. package/dist/knowledge/store.js +193 -7
  33. package/dist/knowledge/store.js.map +1 -1
  34. package/dist/knowledge/types.d.ts +43 -1
  35. package/dist/knowledge/types.d.ts.map +1 -1
  36. package/dist/knowledge/types.js.map +1 -1
  37. package/dist/mcp/tools.d.ts.map +1 -1
  38. package/dist/mcp/tools.js +365 -0
  39. package/dist/mcp/tools.js.map +1 -1
  40. package/docs/API.md +173 -0
  41. package/docs/DEMO_SCRIPT.md +210 -0
  42. package/docs/MCP_TEST_GUIDE.md +414 -0
  43. package/docs/SYNC_GUIDE.md +242 -0
  44. package/package.json +4 -1
  45. package/src/api/server.ts +160 -0
  46. package/src/api/start-server.ts +78 -0
  47. package/src/cli/index.ts +534 -0
  48. package/src/core/context-service.ts +182 -0
  49. package/src/core/index.ts +7 -0
  50. package/src/core/llm-service.ts +359 -0
  51. package/src/knowledge/store.ts +232 -7
  52. package/src/knowledge/types.ts +45 -1
  53. package/src/mcp/tools.ts +415 -0
  54. package/test-glossary.yaml +55 -0
  55. package/test-mcp.db +0 -0
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
  */