@team-semicolon/semo-cli 4.3.0 → 4.4.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.
package/dist/index.js CHANGED
@@ -64,6 +64,7 @@ const get_1 = require("./commands/get");
64
64
  const sessions_1 = require("./commands/sessions");
65
65
  const db_1 = require("./commands/db");
66
66
  const memory_1 = require("./commands/memory");
67
+ const test_1 = require("./commands/test");
67
68
  const global_cache_1 = require("./global-cache");
68
69
  const PACKAGE_NAME = "@team-semicolon/semo-cli";
69
70
  // package.json에서 버전 동적 로드
@@ -778,11 +779,7 @@ program
778
779
  if (!options.skipMcp) {
779
780
  await setupMCP(os.homedir(), [], options.force || false);
780
781
  }
781
- // 6. semo-kb MCP 유저레벨 등록
782
- if (!options.skipMcp) {
783
- await setupSemoKbMcp();
784
- }
785
- // 7. 글로벌 CLAUDE.md에 KB-First 규칙 주입
782
+ // 6. 글로벌 CLAUDE.md에 KB-First 규칙 주입
786
783
  await injectKbFirstToGlobalClaudeMd();
787
784
  await (0, database_1.closeConnection)();
788
785
  // 결과 요약
@@ -793,7 +790,7 @@ program
793
790
  console.log(chalk_1.default.gray(" ~/.claude/commands/ 팀 커맨드 (DB 기반)"));
794
791
  console.log(chalk_1.default.gray(" ~/.claude/agents/ 팀 에이전트 (DB 기반, dedup)"));
795
792
  console.log(chalk_1.default.gray(" ~/.claude/settings.local.json SessionStart/Stop 훅"));
796
- console.log(chalk_1.default.gray(" ~/.claude/settings.json semo-kb MCP (유저레벨, KB-First SoT)"));
793
+ console.log(chalk_1.default.gray(" ~/.claude/settings.json Claude Code 설정 (유저레벨)"));
797
794
  console.log(chalk_1.default.cyan("\n다음 단계:"));
798
795
  console.log(chalk_1.default.gray(" 프로젝트 디렉토리에서 'semo init'을 실행하세요."));
799
796
  console.log();
@@ -1360,29 +1357,87 @@ function registerMCPServer(server) {
1360
1357
  }
1361
1358
  // === 글로벌 CLAUDE.md에 KB-First 규칙 주입 ===
1362
1359
  const KB_FIRST_SECTION_MARKER = "## SEMO KB-First 행동 규칙";
1363
- async function injectKbFirstToGlobalClaudeMd() {
1364
- const globalClaudeMd = path.join(os.homedir(), ".claude", "CLAUDE.md");
1365
- const kbFirstBlock = `
1360
+ async function buildKbFirstBlock() {
1361
+ // DB에서 온톨로지 + 타입스키마를 조회해서 동적 생성
1362
+ let domainGuide = "";
1363
+ try {
1364
+ const pool = (0, database_1.getPool)();
1365
+ // 1. 타입스키마: 타입별 scheme_key 목록
1366
+ const schemaRows = await pool.query(`SELECT type_key, scheme_key, required, scheme_description
1367
+ FROM semo.kb_type_schema ORDER BY type_key, sort_order, scheme_key`);
1368
+ const typeSchemas = new Map();
1369
+ for (const r of schemaRows.rows) {
1370
+ const entries = typeSchemas.get(r.type_key) || [];
1371
+ entries.push({ scheme_key: r.scheme_key, required: r.required, desc: r.scheme_description || "" });
1372
+ typeSchemas.set(r.type_key, entries);
1373
+ }
1374
+ // 2. 온톨로지: 엔티티 타입별 도메인 목록
1375
+ const ontoRows = await pool.query(`SELECT entity_type, domain, description FROM semo.ontology ORDER BY entity_type, domain`);
1376
+ const entities = new Map();
1377
+ for (const r of ontoRows.rows) {
1378
+ const list = entities.get(r.entity_type) || [];
1379
+ list.push({ domain: r.domain, desc: r.description || "" });
1380
+ entities.set(r.entity_type, list);
1381
+ }
1382
+ // 3. 도메인 가이드 생성
1383
+ const orgDomains = entities.get("organization") || [];
1384
+ const svcDomains = entities.get("service") || [];
1385
+ const orgSchema = typeSchemas.get("organization") || [];
1386
+ const svcSchema = typeSchemas.get("service") || [];
1387
+ domainGuide += "#### 도메인 구조\n";
1388
+ domainGuide += "| 패턴 | 예시 | 용도 |\n|------|------|------|\n";
1389
+ if (orgDomains.length > 0) {
1390
+ const orgEx = orgDomains.map(o => o.domain).join(", ");
1391
+ const orgKeys = orgSchema.filter(s => !s.scheme_key.includes("{")).map(s => s.scheme_key).join(", ");
1392
+ domainGuide += `| 조직 도메인 | \`${orgEx}\` | ${orgKeys} 등 조직 정보 |\n`;
1393
+ }
1394
+ if (svcDomains.length > 0) {
1395
+ const svcEx = svcDomains.slice(0, 5).map(s => s.domain).join(", ");
1396
+ const svcKeys = svcSchema.filter(s => !s.scheme_key.includes("{")).map(s => s.scheme_key).join(", ");
1397
+ domainGuide += `| 서비스 도메인 | \`${svcEx}\` 등 ${svcDomains.length}개 | ${svcKeys} 등 서비스 정보 |\n`;
1398
+ }
1399
+ domainGuide += "\n#### 읽기 예시 (Query-First)\n";
1400
+ domainGuide += "다음 주제 질문 → **반드시 `semo kb search`/`semo kb get`으로 KB 먼저 조회** 후 답변:\n";
1401
+ // 조직 도메인 키 가이드
1402
+ for (const s of orgSchema) {
1403
+ if (s.scheme_key.includes("{")) {
1404
+ const label = s.desc || s.scheme_key;
1405
+ domainGuide += `- ${label} → \`domain: ${orgDomains[0]?.domain || "semicolon"}\`, key: \`${s.scheme_key}\`\n`;
1406
+ }
1407
+ }
1408
+ // 서비스 도메인 키 가이드
1409
+ for (const s of svcSchema.filter(s => s.required && !s.scheme_key.includes("{"))) {
1410
+ const label = s.desc || s.scheme_key;
1411
+ domainGuide += `- 서비스 ${label} → \`domain: {서비스명}\`, key: \`${s.scheme_key}\`\n`;
1412
+ }
1413
+ domainGuide += `- 서비스 KPI → \`domain: {서비스명}\`, key: \`kpi/current\`\n`;
1414
+ domainGuide += `- 서비스 마일스톤 → \`domain: {서비스명}\`, key: \`milestone/{slug}\`\n`;
1415
+ }
1416
+ catch {
1417
+ // DB 연결 실패 시 최소한의 가이드
1418
+ domainGuide = `### 읽기 (Query-First)
1419
+ 다음 주제 질문 → **반드시 \`semo kb search\`/\`semo kb get\`으로 KB 먼저 조회** 후 답변.
1420
+ 도메인/키 구조는 \`semo kb ontology --action list\`로 확인 가능.
1421
+ `;
1422
+ }
1423
+ return `
1366
1424
  ${KB_FIRST_SECTION_MARKER}
1367
1425
 
1368
- > semo-kb MCP 서버가 유저레벨에 등록되어 있습니다. KB는 팀의 Single Source of Truth입니다.
1369
-
1370
- ### 읽기 (Query-First)
1371
- 다음 주제 질문 → **반드시 kb_search/kb_get으로 KB 먼저 조회** 후 답변:
1372
- - 팀원 정보 → \`domain: team\`
1373
- - 프로젝트 현황 → \`domain: project\`
1374
- - 의사결정 기록 → \`domain: decision\`
1375
- - 업무 프로세스 → \`domain: process\`
1376
- - 인프라 구성 → \`domain: infra\`
1377
- - KPI → \`domain: kpi\`
1426
+ > semo CLI의 kb-manager 스킬을 통해 KB에 접근합니다. KB는 팀의 Single Source of Truth입니다.
1427
+ > 도메인/키 구조가 변경될 수 있으므로 \`semo kb ontology --action list\`로 최신 구조를 확인하세요.
1378
1428
 
1429
+ ${domainGuide}
1379
1430
  **금지:** 위 주제를 자체 지식/세션 기억만으로 답변하는 것.
1380
1431
  KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
1381
1432
 
1382
1433
  ### 쓰기 (Write-Back)
1383
- 사용자가 팀 정보를 정정/추가/변경하면, 의사결정이 내려지면 → **반드시 kb_upsert로 KB에 즉시 기록.**
1434
+ 사용자가 팀 정보를 정정/추가/변경하면, 의사결정이 내려지면 → **반드시 \`semo kb upsert\`로 KB에 즉시 기록.**
1384
1435
  **금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
1385
1436
  `;
1437
+ }
1438
+ async function injectKbFirstToGlobalClaudeMd() {
1439
+ const globalClaudeMd = path.join(os.homedir(), ".claude", "CLAUDE.md");
1440
+ const kbFirstBlock = await buildKbFirstBlock();
1386
1441
  if (fs.existsSync(globalClaudeMd)) {
1387
1442
  const content = fs.readFileSync(globalClaudeMd, "utf-8");
1388
1443
  if (content.includes(KB_FIRST_SECTION_MARKER)) {
@@ -1404,50 +1459,6 @@ KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하
1404
1459
  console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md 생성됨 (KB-First 규칙)"));
1405
1460
  }
1406
1461
  }
1407
- // === semo-kb MCP 유저레벨 등록 ===
1408
- async function setupSemoKbMcp() {
1409
- console.log(chalk_1.default.cyan("\n📡 semo-kb MCP 유저레벨 등록"));
1410
- console.log(chalk_1.default.gray(" KB-First SoT — 어디서든 KB 조회/갱신 가능\n"));
1411
- // semo-kb MCP 서버 경로 탐색
1412
- // 1. cwd에서 packages/mcp-kb/dist/index.js 찾기
1413
- // 2. CLI 패키지 기준으로 monorepo 루트 탐색
1414
- // 3. 환경변수 SEMO_PROJECT_ROOT
1415
- const candidates = [
1416
- path.join(process.cwd(), "packages", "mcp-kb", "dist", "index.js"),
1417
- process.env.SEMO_PROJECT_ROOT
1418
- ? path.join(process.env.SEMO_PROJECT_ROOT, "packages", "mcp-kb", "dist", "index.js")
1419
- : "",
1420
- path.resolve(__dirname, "..", "..", "..", "mcp-kb", "dist", "index.js"),
1421
- ].filter(Boolean);
1422
- const mcpEntryPath = candidates.find((p) => fs.existsSync(p));
1423
- if (!mcpEntryPath) {
1424
- console.log(chalk_1.default.yellow(" ⚠ semo-kb MCP 서버를 찾을 수 없습니다."));
1425
- console.log(chalk_1.default.gray(" semo 프로젝트 루트에서 실행하거나 SEMO_PROJECT_ROOT 환경변수를 설정하세요."));
1426
- console.log(chalk_1.default.gray(" 예: cd /path/to/semo && semo onboarding"));
1427
- return;
1428
- }
1429
- const absolutePath = path.resolve(mcpEntryPath);
1430
- console.log(chalk_1.default.gray(` 경로: ${absolutePath}`));
1431
- // claude mcp add로 유저레벨 등록
1432
- const result = registerMCPServer({
1433
- name: "semo-kb",
1434
- command: "node",
1435
- args: [absolutePath],
1436
- scope: "user",
1437
- });
1438
- if (result.success) {
1439
- if (result.skipped) {
1440
- console.log(chalk_1.default.gray(" semo-kb 이미 등록됨 (건너뜀)"));
1441
- }
1442
- else {
1443
- console.log(chalk_1.default.green(" ✓ semo-kb MCP 유저레벨 등록 완료"));
1444
- }
1445
- }
1446
- else {
1447
- console.log(chalk_1.default.yellow(` ⚠ semo-kb 등록 실패: ${result.error}`));
1448
- console.log(chalk_1.default.gray(" 수동 등록: claude mcp add semo-kb -s user -- node " + absolutePath));
1449
- }
1450
- }
1451
1462
  // === MCP 설정 ===
1452
1463
  async function setupMCP(cwd, _extensions, force) {
1453
1464
  console.log(chalk_1.default.cyan("\n🔧 Black Box 설정 (MCP Server)"));
@@ -1464,8 +1475,7 @@ async function setupMCP(cwd, _extensions, force) {
1464
1475
  const settings = {
1465
1476
  mcpServers: {},
1466
1477
  };
1467
- // semo-kb는 유저레벨에서 등록 (semo onboarding)하므로 프로젝트 settings에 쓰지 않음
1468
- // 공통 서버(context7 등)도 유저레벨에 등록하므로 프로젝트 settings에 쓰지 않음
1478
+ // 공통 서버(context7 ) 유저레벨에 등록하므로 프로젝트 settings에 쓰지 않음
1469
1479
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1470
1480
  console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨"));
1471
1481
  // Claude Code에 MCP 서버 등록 시도 (공통 서버는 유저레벨로)
@@ -1786,6 +1796,13 @@ async function setupClaudeMd(cwd, _extensions, force) {
1786
1796
  return;
1787
1797
  }
1788
1798
  }
1799
+ // KB-First 섹션을 DB에서 동적 생성
1800
+ const kbFirstFull = await buildKbFirstBlock();
1801
+ // 프로젝트용: "## SEMO KB-First 행동 규칙" → "## KB-First 행동 규칙 (NON-NEGOTIABLE)"
1802
+ const kbFirstSection = kbFirstFull
1803
+ .replace(KB_FIRST_SECTION_MARKER, "## KB-First 행동 규칙 (NON-NEGOTIABLE)")
1804
+ .replace(/> semo CLI의 kb-manager 스킬을 통해.*\n/, "> KB는 팀의 Single Source of Truth이다. 아래 규칙은 예외 없이 적용된다.\n")
1805
+ .trim();
1789
1806
  // 프로젝트 규칙만 (스킬/에이전트 목록은 글로벌 ~/.claude/에 있음)
1790
1807
  const claudeMdContent = `# SEMO Project Configuration
1791
1808
 
@@ -1794,45 +1811,21 @@ async function setupClaudeMd(cwd, _extensions, force) {
1794
1811
 
1795
1812
  ---
1796
1813
 
1797
- ## KB 접근 (semo-kb MCP 서버)
1814
+ ## KB 접근 (semo CLI)
1798
1815
 
1799
- KB 데이터는 **semo-kb MCP 서버**를 통해 Core DB에서 실시간 조회합니다.
1816
+ KB 데이터는 **semo CLI** (\`kb-manager\` 스킬)를 통해 Core DB에서 조회합니다.
1800
1817
 
1801
- | MCP 도구 | 설명 |
1802
- |----------|------|
1803
- | \`kb_search\` | 벡터+텍스트 하이브리드 검색 (query, domain?, limit?, mode?) |
1804
- | \`kb_get\` | domain+key 정확 조회 |
1805
- | \`kb_list\` | 도메인별 엔트리 목록 |
1806
- | \`kb_upsert\` | KB 항목 쓰기 (OpenAI 임베딩 자동 생성) |
1807
- | \`kb_bot_status\` | 상태 테이블 조회 |
1808
- | \`kb_ontology\` | 온톨로지 스키마 조회 |
1809
- | \`kb_digest\` | 봇 구독 도메인 변경 다이제스트 |
1818
+ | 명령어 | 설명 |
1819
+ |--------|------|
1820
+ | \`semo kb search "쿼리"\` | 벡터+텍스트 하이브리드 검색 |
1821
+ | \`semo kb get <domain> <key> [sub_key]\` | domain+key 정확 조회 |
1822
+ | \`semo kb list --domain <domain>\` | 도메인별 엔트리 목록 |
1823
+ | \`semo kb upsert <domain> <key> [sub_key] --content "내용"\` | KB 항목 쓰기 |
1824
+ | \`semo kb ontology --action <action>\` | 온톨로지 조회 |
1810
1825
 
1811
1826
  ---
1812
1827
 
1813
- ## KB-First 행동 규칙 (NON-NEGOTIABLE)
1814
-
1815
- > KB는 팀의 Single Source of Truth이다. 아래 규칙은 예외 없이 적용된다.
1816
-
1817
- ### 읽기 (Query-First)
1818
- 다음 주제 질문 → **반드시 kb_search/kb_get으로 KB 먼저 조회** 후 답변:
1819
- - 팀원 정보 → \`domain: team\`
1820
- - 프로젝트 현황 → \`domain: project\`
1821
- - 의사결정 기록 → \`domain: decision\`
1822
- - 업무 프로세스 → \`domain: process\`
1823
- - 인프라 구성 → \`domain: infra\`
1824
- - KPI → \`domain: kpi\`
1825
-
1826
- **금지:** 위 주제를 자체 지식/세션 기억만으로 답변하는 것.
1827
- KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
1828
-
1829
- ### 쓰기 (Write-Back)
1830
- 다음 상황 → **반드시 kb_upsert로 KB에 즉시 기록:**
1831
- - 사용자가 팀 정보를 정정하거나 새 사실을 알려줄 때
1832
- - 의사결정이 내려졌을 때
1833
- - 프로세스/규칙이 변경되었을 때
1834
-
1835
- **금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
1828
+ ${kbFirstSection}
1836
1829
 
1837
1830
  ---
1838
1831
 
@@ -2582,6 +2575,242 @@ kbCmd
2582
2575
  process.exit(1);
2583
2576
  }
2584
2577
  });
2578
+ kbCmd
2579
+ .command("get <domain> <key> [sub_key]")
2580
+ .description("KB 단일 항목 정확 조회 (domain + key + sub_key)")
2581
+ .option("--format <type>", "출력 형식 (json|table)", "json")
2582
+ .action(async (domain, key, subKey, options) => {
2583
+ try {
2584
+ const pool = (0, database_1.getPool)();
2585
+ const entry = await (0, kb_1.kbGet)(pool, domain, key, subKey);
2586
+ if (!entry) {
2587
+ console.log(chalk_1.default.yellow(`\n 항목 없음: ${domain}/${key}${subKey ? '/' + subKey : ''}\n`));
2588
+ await (0, database_1.closeConnection)();
2589
+ process.exit(1);
2590
+ }
2591
+ if (options.format === "json") {
2592
+ console.log(JSON.stringify(entry, null, 2));
2593
+ }
2594
+ else {
2595
+ console.log(chalk_1.default.cyan.bold(`\n📄 [${entry.domain}] ${entry.key}${entry.sub_key ? '/' + entry.sub_key : ''}\n`));
2596
+ console.log(entry.content);
2597
+ if (entry.metadata && Object.keys(entry.metadata).length > 0) {
2598
+ console.log(chalk_1.default.gray(`\n metadata: ${JSON.stringify(entry.metadata)}`));
2599
+ }
2600
+ if (entry.updated_at) {
2601
+ console.log(chalk_1.default.gray(` updated: ${entry.updated_at}`));
2602
+ }
2603
+ console.log();
2604
+ }
2605
+ await (0, database_1.closeConnection)();
2606
+ }
2607
+ catch (err) {
2608
+ console.error(chalk_1.default.red(`조회 실패: ${err}`));
2609
+ await (0, database_1.closeConnection)();
2610
+ process.exit(1);
2611
+ }
2612
+ });
2613
+ kbCmd
2614
+ .command("upsert <domain> <key> [sub_key]")
2615
+ .description("KB 항목 쓰기 (upsert) — 임베딩 자동 생성 + 스키마 검증")
2616
+ .requiredOption("--content <text>", "항목 본문")
2617
+ .option("--metadata <json>", "추가 메타데이터 (JSON 문자열)")
2618
+ .option("--created-by <name>", "작성자 식별자", "semo-cli")
2619
+ .action(async (domain, key, subKey, options) => {
2620
+ const spinner = (0, ora_1.default)("KB upsert 중...").start();
2621
+ try {
2622
+ const pool = (0, database_1.getPool)();
2623
+ const metadata = options.metadata ? JSON.parse(options.metadata) : undefined;
2624
+ const result = await (0, kb_1.kbUpsert)(pool, {
2625
+ domain,
2626
+ key,
2627
+ sub_key: subKey,
2628
+ content: options.content,
2629
+ metadata,
2630
+ created_by: options.createdBy,
2631
+ });
2632
+ if (result.success) {
2633
+ spinner.succeed(`KB upsert 완료: ${domain}/${key}${subKey ? '/' + subKey : ''}`);
2634
+ if (result.warnings && result.warnings.length > 0) {
2635
+ for (const w of result.warnings) {
2636
+ console.log(chalk_1.default.yellow(` ⚠️ ${w}`));
2637
+ }
2638
+ }
2639
+ }
2640
+ else {
2641
+ spinner.fail(`KB upsert 실패: ${result.error}`);
2642
+ process.exit(1);
2643
+ }
2644
+ await (0, database_1.closeConnection)();
2645
+ }
2646
+ catch (err) {
2647
+ spinner.fail(`KB upsert 실패: ${err}`);
2648
+ await (0, database_1.closeConnection)();
2649
+ process.exit(1);
2650
+ }
2651
+ });
2652
+ kbCmd
2653
+ .command("ontology")
2654
+ .description("온톨로지 조회 — 도메인/타입/스키마/라우팅 테이블")
2655
+ .option("--action <type>", "조회 동작 (list|show|services|types|instances|schema|routing-table)", "list")
2656
+ .option("--domain <name>", "action=show 시 도메인")
2657
+ .option("--type <name>", "action=schema 시 타입 키")
2658
+ .option("--format <type>", "출력 형식 (json|table)", "table")
2659
+ .action(async (options) => {
2660
+ try {
2661
+ const pool = (0, database_1.getPool)();
2662
+ const action = options.action;
2663
+ if (action === "list") {
2664
+ const domains = await (0, kb_1.ontoList)(pool);
2665
+ if (options.format === "json") {
2666
+ console.log(JSON.stringify(domains, null, 2));
2667
+ }
2668
+ else {
2669
+ console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 도메인\n"));
2670
+ for (const d of domains) {
2671
+ const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
2672
+ const svcStr = d.service ? chalk_1.default.gray(` (${d.service})`) : "";
2673
+ console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + svcStr);
2674
+ if (d.description)
2675
+ console.log(chalk_1.default.gray(` ${d.description}`));
2676
+ }
2677
+ console.log();
2678
+ }
2679
+ }
2680
+ else if (action === "show") {
2681
+ if (!options.domain) {
2682
+ console.log(chalk_1.default.red("--domain 옵션이 필요합니다."));
2683
+ process.exit(1);
2684
+ }
2685
+ const onto = await (0, kb_1.ontoShow)(pool, options.domain);
2686
+ if (!onto) {
2687
+ console.log(chalk_1.default.red(`온톨로지 '${options.domain}'을 찾을 수 없습니다.`));
2688
+ process.exit(1);
2689
+ }
2690
+ if (options.format === "json") {
2691
+ console.log(JSON.stringify(onto, null, 2));
2692
+ }
2693
+ else {
2694
+ console.log(chalk_1.default.cyan.bold(`\n📐 온톨로지: ${onto.domain}\n`));
2695
+ if (onto.description)
2696
+ console.log(chalk_1.default.white(` ${onto.description}`));
2697
+ console.log(chalk_1.default.gray(` 버전: ${onto.version}`));
2698
+ console.log(chalk_1.default.gray(` 스키마:\n`));
2699
+ console.log(chalk_1.default.white(JSON.stringify(onto.schema, null, 2).split("\n").map(l => " " + l).join("\n")));
2700
+ console.log();
2701
+ }
2702
+ }
2703
+ else if (action === "services") {
2704
+ const services = await (0, kb_1.ontoListServices)(pool);
2705
+ if (options.format === "json") {
2706
+ console.log(JSON.stringify(services, null, 2));
2707
+ }
2708
+ else {
2709
+ console.log(chalk_1.default.cyan.bold("\n📐 서비스 목록\n"));
2710
+ for (const s of services) {
2711
+ console.log(chalk_1.default.cyan(` ${s.service}`) + chalk_1.default.gray(` (${s.domain_count} domains)`));
2712
+ console.log(chalk_1.default.gray(` ${s.domains.join(", ")}`));
2713
+ }
2714
+ console.log();
2715
+ }
2716
+ }
2717
+ else if (action === "types") {
2718
+ const types = await (0, kb_1.ontoListTypes)(pool);
2719
+ if (options.format === "json") {
2720
+ console.log(JSON.stringify(types, null, 2));
2721
+ }
2722
+ else {
2723
+ console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 타입\n"));
2724
+ for (const t of types) {
2725
+ console.log(chalk_1.default.cyan(` ${t.type_key}`) + chalk_1.default.gray(` (v${t.version})`));
2726
+ if (t.description)
2727
+ console.log(chalk_1.default.gray(` ${t.description}`));
2728
+ }
2729
+ console.log();
2730
+ }
2731
+ }
2732
+ else if (action === "instances") {
2733
+ const instances = await (0, kb_1.ontoListInstances)(pool);
2734
+ if (options.format === "json") {
2735
+ console.log(JSON.stringify(instances, null, 2));
2736
+ }
2737
+ else {
2738
+ console.log(chalk_1.default.cyan.bold("\n📐 서비스 인스턴스\n"));
2739
+ for (const inst of instances) {
2740
+ console.log(chalk_1.default.cyan(` ${inst.domain}`) + chalk_1.default.gray(` (${inst.entry_count} entries)`));
2741
+ if (inst.description)
2742
+ console.log(chalk_1.default.gray(` ${inst.description}`));
2743
+ if (inst.scoped_domains.length > 0) {
2744
+ console.log(chalk_1.default.gray(` scoped: ${inst.scoped_domains.join(", ")}`));
2745
+ }
2746
+ }
2747
+ console.log();
2748
+ }
2749
+ }
2750
+ else if (action === "schema") {
2751
+ if (!options.type) {
2752
+ console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type service)"));
2753
+ process.exit(1);
2754
+ }
2755
+ const schema = await (0, kb_1.ontoListSchema)(pool, options.type);
2756
+ if (options.format === "json") {
2757
+ console.log(JSON.stringify(schema, null, 2));
2758
+ }
2759
+ else {
2760
+ console.log(chalk_1.default.cyan.bold(`\n📐 타입 스키마: ${options.type}\n`));
2761
+ if (schema.length === 0) {
2762
+ console.log(chalk_1.default.yellow(` 스키마 없음: '${options.type}'`));
2763
+ }
2764
+ else {
2765
+ for (const s of schema) {
2766
+ const reqStr = s.required ? chalk_1.default.red(" *") : "";
2767
+ const typeStr = chalk_1.default.gray(` [${s.key_type}]`);
2768
+ console.log(chalk_1.default.cyan(` ${s.scheme_key}`) + typeStr + reqStr);
2769
+ if (s.scheme_description)
2770
+ console.log(chalk_1.default.gray(` ${s.scheme_description}`));
2771
+ if (s.value_hint)
2772
+ console.log(chalk_1.default.gray(` hint: ${s.value_hint}`));
2773
+ }
2774
+ }
2775
+ console.log();
2776
+ }
2777
+ }
2778
+ else if (action === "routing-table") {
2779
+ const table = await (0, kb_1.ontoRoutingTable)(pool);
2780
+ if (options.format === "json") {
2781
+ console.log(JSON.stringify(table, null, 2));
2782
+ }
2783
+ else {
2784
+ console.log(chalk_1.default.cyan.bold("\n📐 라우팅 테이블\n"));
2785
+ let lastDomain = "";
2786
+ for (const r of table) {
2787
+ if (r.domain !== lastDomain) {
2788
+ lastDomain = r.domain;
2789
+ const svcStr = r.service ? chalk_1.default.gray(` (${r.service})`) : "";
2790
+ console.log(chalk_1.default.white.bold(`\n ${r.domain}`) + chalk_1.default.gray(` [${r.entity_type}]`) + svcStr);
2791
+ if (r.domain_description)
2792
+ console.log(chalk_1.default.gray(` ${r.domain_description}`));
2793
+ }
2794
+ const typeStr = chalk_1.default.gray(` [${r.key_type}]`);
2795
+ console.log(chalk_1.default.cyan(` → ${r.scheme_key}`) + typeStr);
2796
+ if (r.scheme_description)
2797
+ console.log(chalk_1.default.gray(` ${r.scheme_description}`));
2798
+ }
2799
+ console.log();
2800
+ }
2801
+ }
2802
+ else {
2803
+ console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table`));
2804
+ process.exit(1);
2805
+ }
2806
+ await (0, database_1.closeConnection)();
2807
+ }
2808
+ catch (err) {
2809
+ console.error(chalk_1.default.red(`온톨로지 조회 실패: ${err}`));
2810
+ await (0, database_1.closeConnection)();
2811
+ process.exit(1);
2812
+ }
2813
+ });
2585
2814
  // === Ontology 관리 ===
2586
2815
  const ontoCmd = program
2587
2816
  .command("onto")
@@ -2607,11 +2836,11 @@ ontoCmd
2607
2836
  console.log(chalk_1.default.yellow(" 온톨로지가 정의되지 않았습니다."));
2608
2837
  }
2609
2838
  else {
2610
- // Group by service
2611
- const global = domains.filter(d => !d.service);
2839
+ // Group by service (_global treated as Global)
2840
+ const global = domains.filter(d => !d.service || d.service === '_global');
2612
2841
  const byService = {};
2613
2842
  for (const d of domains) {
2614
- if (d.service) {
2843
+ if (d.service && d.service !== '_global') {
2615
2844
  if (!byService[d.service])
2616
2845
  byService[d.service] = [];
2617
2846
  byService[d.service].push(d);
@@ -2758,6 +2987,7 @@ ontoCmd
2758
2987
  (0, sessions_1.registerSessionsCommands)(program);
2759
2988
  (0, db_1.registerDbCommands)(program);
2760
2989
  (0, memory_1.registerMemoryCommands)(program);
2990
+ (0, test_1.registerTestCommands)(program);
2761
2991
  // === semo skills — DB 시딩 ===
2762
2992
  /**
2763
2993
  * SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
package/dist/kb.d.ts CHANGED
@@ -20,6 +20,7 @@ export declare function generateEmbeddings(texts: string[]): Promise<(number[] |
20
20
  export interface KBEntry {
21
21
  domain: string;
22
22
  key: string;
23
+ sub_key?: string;
23
24
  content: string;
24
25
  metadata?: Record<string, unknown>;
25
26
  created_by?: string;
@@ -54,6 +55,7 @@ export interface KBStatusInfo {
54
55
  export interface KBDigestEntry {
55
56
  domain: string;
56
57
  key: string;
58
+ sub_key?: string;
57
59
  content: string;
58
60
  version: number;
59
61
  change_type: 'new' | 'updated';
@@ -129,6 +131,73 @@ export declare function ontoValidate(pool: Pool, domain: string, entries?: KBEnt
129
131
  * Returns all KB changes since the given ISO timestamp.
130
132
  */
131
133
  export declare function kbDigest(pool: Pool, since: string, domain?: string): Promise<KBDigestResult>;
134
+ /**
135
+ * Get a single KB entry by domain + key + sub_key
136
+ */
137
+ export declare function kbGet(pool: Pool, domain: string, rawKey: string, rawSubKey?: string): Promise<KBEntry | null>;
138
+ /**
139
+ * Upsert a single KB entry with domain/key validation and embedding generation
140
+ */
141
+ export declare function kbUpsert(pool: Pool, entry: {
142
+ domain: string;
143
+ key: string;
144
+ sub_key?: string;
145
+ content: string;
146
+ metadata?: Record<string, unknown>;
147
+ created_by?: string;
148
+ }): Promise<{
149
+ success: boolean;
150
+ error?: string;
151
+ warnings?: string[];
152
+ }>;
153
+ export interface TypeSchemaEntry {
154
+ type_key: string;
155
+ scheme_key: string;
156
+ scheme_description: string;
157
+ required: boolean;
158
+ value_hint: string | null;
159
+ sort_order: number;
160
+ key_type: "singleton" | "collection";
161
+ }
162
+ export interface RoutingEntry {
163
+ domain: string;
164
+ entity_type: string;
165
+ service: string | null;
166
+ domain_description: string | null;
167
+ scheme_key: string;
168
+ key_type: string;
169
+ scheme_description: string;
170
+ value_hint: string | null;
171
+ }
172
+ export interface ServiceInfo {
173
+ service: string;
174
+ domain_count: number;
175
+ domains: string[];
176
+ }
177
+ export interface ServiceInstance {
178
+ domain: string;
179
+ description: string | null;
180
+ service: string;
181
+ tags: string[];
182
+ scoped_domains: string[];
183
+ entry_count: number;
184
+ }
185
+ /**
186
+ * List type schema entries for a given entity type
187
+ */
188
+ export declare function ontoListSchema(pool: Pool, typeKey: string): Promise<TypeSchemaEntry[]>;
189
+ /**
190
+ * Full domain→key routing table for bot auto-classification
191
+ */
192
+ export declare function ontoRoutingTable(pool: Pool): Promise<RoutingEntry[]>;
193
+ /**
194
+ * List services grouped with domain counts
195
+ */
196
+ export declare function ontoListServices(pool: Pool): Promise<ServiceInfo[]>;
197
+ /**
198
+ * List service instances (entity_type = 'service')
199
+ */
200
+ export declare function ontoListInstances(pool: Pool): Promise<ServiceInstance[]>;
132
201
  /**
133
202
  * Write ontology schemas to local cache
134
203
  */