@team-semicolon/semo-cli 4.2.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
@@ -63,6 +63,8 @@ const bots_1 = require("./commands/bots");
63
63
  const get_1 = require("./commands/get");
64
64
  const sessions_1 = require("./commands/sessions");
65
65
  const db_1 = require("./commands/db");
66
+ const memory_1 = require("./commands/memory");
67
+ const test_1 = require("./commands/test");
66
68
  const global_cache_1 = require("./global-cache");
67
69
  const PACKAGE_NAME = "@team-semicolon/semo-cli";
68
70
  // package.json에서 버전 동적 로드
@@ -773,10 +775,12 @@ program
773
775
  await setupStandardGlobal();
774
776
  // 4. Hooks 설치 (프로젝트 무관)
775
777
  await setupHooks(false);
776
- // 5. MCP 설정 (글로벌)
778
+ // 5. MCP 설정 (글로벌 공통 서버)
777
779
  if (!options.skipMcp) {
778
780
  await setupMCP(os.homedir(), [], options.force || false);
779
781
  }
782
+ // 6. 글로벌 CLAUDE.md에 KB-First 규칙 주입
783
+ await injectKbFirstToGlobalClaudeMd();
780
784
  await (0, database_1.closeConnection)();
781
785
  // 결과 요약
782
786
  console.log(chalk_1.default.green.bold("\n✅ SEMO 글로벌 온보딩 완료!\n"));
@@ -786,6 +790,7 @@ program
786
790
  console.log(chalk_1.default.gray(" ~/.claude/commands/ 팀 커맨드 (DB 기반)"));
787
791
  console.log(chalk_1.default.gray(" ~/.claude/agents/ 팀 에이전트 (DB 기반, dedup)"));
788
792
  console.log(chalk_1.default.gray(" ~/.claude/settings.local.json SessionStart/Stop 훅"));
793
+ console.log(chalk_1.default.gray(" ~/.claude/settings.json Claude Code 설정 (유저레벨)"));
789
794
  console.log(chalk_1.default.cyan("\n다음 단계:"));
790
795
  console.log(chalk_1.default.gray(" 프로젝트 디렉토리에서 'semo init'을 실행하세요."));
791
796
  console.log();
@@ -1350,6 +1355,110 @@ function registerMCPServer(server) {
1350
1355
  return { success: false, error: String(error) };
1351
1356
  }
1352
1357
  }
1358
+ // === 글로벌 CLAUDE.md에 KB-First 규칙 주입 ===
1359
+ const KB_FIRST_SECTION_MARKER = "## SEMO KB-First 행동 규칙";
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 `
1424
+ ${KB_FIRST_SECTION_MARKER}
1425
+
1426
+ > semo CLI의 kb-manager 스킬을 통해 KB에 접근합니다. KB는 팀의 Single Source of Truth입니다.
1427
+ > 도메인/키 구조가 변경될 수 있으므로 \`semo kb ontology --action list\`로 최신 구조를 확인하세요.
1428
+
1429
+ ${domainGuide}
1430
+ **금지:** 위 주제를 자체 지식/세션 기억만으로 답변하는 것.
1431
+ KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
1432
+
1433
+ ### 쓰기 (Write-Back)
1434
+ 사용자가 팀 정보를 정정/추가/변경하면, 의사결정이 내려지면 → **반드시 \`semo kb upsert\`로 KB에 즉시 기록.**
1435
+ **금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
1436
+ `;
1437
+ }
1438
+ async function injectKbFirstToGlobalClaudeMd() {
1439
+ const globalClaudeMd = path.join(os.homedir(), ".claude", "CLAUDE.md");
1440
+ const kbFirstBlock = await buildKbFirstBlock();
1441
+ if (fs.existsSync(globalClaudeMd)) {
1442
+ const content = fs.readFileSync(globalClaudeMd, "utf-8");
1443
+ if (content.includes(KB_FIRST_SECTION_MARKER)) {
1444
+ // 기존 섹션 교체
1445
+ const regex = new RegExp(`\\n${KB_FIRST_SECTION_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?(?=\\n## |$)`, "m");
1446
+ const updated = content.replace(regex, kbFirstBlock);
1447
+ fs.writeFileSync(globalClaudeMd, updated);
1448
+ console.log(chalk_1.default.gray(" ~/.claude/CLAUDE.md KB-First 규칙 업데이트됨"));
1449
+ }
1450
+ else {
1451
+ // 끝에 추가
1452
+ fs.writeFileSync(globalClaudeMd, content.trimEnd() + "\n" + kbFirstBlock);
1453
+ console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md에 KB-First 규칙 추가됨"));
1454
+ }
1455
+ }
1456
+ else {
1457
+ // 파일 없으면 생성
1458
+ fs.writeFileSync(globalClaudeMd, kbFirstBlock.trim() + "\n");
1459
+ console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md 생성됨 (KB-First 규칙)"));
1460
+ }
1461
+ }
1353
1462
  // === MCP 설정 ===
1354
1463
  async function setupMCP(cwd, _extensions, force) {
1355
1464
  console.log(chalk_1.default.cyan("\n🔧 Black Box 설정 (MCP Server)"));
@@ -1366,14 +1475,9 @@ async function setupMCP(cwd, _extensions, force) {
1366
1475
  const settings = {
1367
1476
  mcpServers: {},
1368
1477
  };
1369
- // settings.json에는 프로젝트 전용 semo-kb만 기록
1370
1478
  // 공통 서버(context7 등)는 유저레벨에 등록하므로 프로젝트 settings에 쓰지 않음
1371
- settings.mcpServers["semo-kb"] = {
1372
- command: "node",
1373
- args: ["packages/mcp-kb/dist/index.js"],
1374
- };
1375
1479
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1376
- console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨 (semo-kb MCP 설정)"));
1480
+ console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨"));
1377
1481
  // Claude Code에 MCP 서버 등록 시도 (공통 서버는 유저레벨로)
1378
1482
  console.log(chalk_1.default.cyan("\n🔌 Claude Code에 MCP 서버 등록 중..."));
1379
1483
  const allServers = [...BASE_MCP_SERVERS];
@@ -1692,6 +1796,13 @@ async function setupClaudeMd(cwd, _extensions, force) {
1692
1796
  return;
1693
1797
  }
1694
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();
1695
1806
  // 프로젝트 규칙만 (스킬/에이전트 목록은 글로벌 ~/.claude/에 있음)
1696
1807
  const claudeMdContent = `# SEMO Project Configuration
1697
1808
 
@@ -1700,45 +1811,21 @@ async function setupClaudeMd(cwd, _extensions, force) {
1700
1811
 
1701
1812
  ---
1702
1813
 
1703
- ## KB 접근 (semo-kb MCP 서버)
1814
+ ## KB 접근 (semo CLI)
1704
1815
 
1705
- KB 데이터는 **semo-kb MCP 서버**를 통해 Core DB에서 실시간 조회합니다.
1816
+ KB 데이터는 **semo CLI** (\`kb-manager\` 스킬)를 통해 Core DB에서 조회합니다.
1706
1817
 
1707
- | MCP 도구 | 설명 |
1708
- |----------|------|
1709
- | \`kb_search\` | 벡터+텍스트 하이브리드 검색 (query, domain?, limit?, mode?) |
1710
- | \`kb_get\` | domain+key 정확 조회 |
1711
- | \`kb_list\` | 도메인별 엔트리 목록 |
1712
- | \`kb_upsert\` | KB 항목 쓰기 (OpenAI 임베딩 자동 생성) |
1713
- | \`kb_bot_status\` | 상태 테이블 조회 |
1714
- | \`kb_ontology\` | 온톨로지 스키마 조회 |
1715
- | \`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>\` | 온톨로지 조회 |
1716
1825
 
1717
1826
  ---
1718
1827
 
1719
- ## KB-First 행동 규칙 (NON-NEGOTIABLE)
1720
-
1721
- > KB는 팀의 Single Source of Truth이다. 아래 규칙은 예외 없이 적용된다.
1722
-
1723
- ### 읽기 (Query-First)
1724
- 다음 주제 질문 → **반드시 kb_search/kb_get으로 KB 먼저 조회** 후 답변:
1725
- - 팀원 정보 → \`domain: team\`
1726
- - 프로젝트 현황 → \`domain: project\`
1727
- - 의사결정 기록 → \`domain: decision\`
1728
- - 업무 프로세스 → \`domain: process\`
1729
- - 인프라 구성 → \`domain: infra\`
1730
- - KPI → \`domain: kpi\`
1731
-
1732
- **금지:** 위 주제를 자체 지식/세션 기억만으로 답변하는 것.
1733
- KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
1734
-
1735
- ### 쓰기 (Write-Back)
1736
- 다음 상황 → **반드시 kb_upsert로 KB에 즉시 기록:**
1737
- - 사용자가 팀 정보를 정정하거나 새 사실을 알려줄 때
1738
- - 의사결정이 내려졌을 때
1739
- - 프로세스/규칙이 변경되었을 때
1740
-
1741
- **금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
1828
+ ${kbFirstSection}
1742
1829
 
1743
1830
  ---
1744
1831
 
@@ -2331,6 +2418,7 @@ kbCmd
2331
2418
  .command("list")
2332
2419
  .description("KB 항목 목록 조회")
2333
2420
  .option("--domain <name>", "도메인 필터")
2421
+ .option("--service <name>", "서비스(프로젝트) 필터 — 해당 서비스의 모든 도메인 항목 반환")
2334
2422
  .option("--limit <n>", "최대 항목 수", "50")
2335
2423
  .option("--format <type>", "출력 형식 (table|json)", "table")
2336
2424
  .action(async (options) => {
@@ -2338,6 +2426,7 @@ kbCmd
2338
2426
  const pool = (0, database_1.getPool)();
2339
2427
  const entries = await (0, kb_1.kbList)(pool, {
2340
2428
  domain: options.domain,
2429
+ service: options.service,
2341
2430
  limit: parseInt(options.limit),
2342
2431
  });
2343
2432
  if (options.format === "json") {
@@ -2370,6 +2459,7 @@ kbCmd
2370
2459
  .command("search <query>")
2371
2460
  .description("KB 검색 (시맨틱 + 텍스트 하이브리드)")
2372
2461
  .option("--domain <name>", "도메인 필터")
2462
+ .option("--service <name>", "서비스(프로젝트) 필터")
2373
2463
  .option("--limit <n>", "최대 결과 수", "10")
2374
2464
  .option("--mode <type>", "검색 모드 (hybrid|semantic|text)", "hybrid")
2375
2465
  .action(async (query, options) => {
@@ -2378,6 +2468,7 @@ kbCmd
2378
2468
  const pool = (0, database_1.getPool)();
2379
2469
  const results = await (0, kb_1.kbSearch)(pool, query, {
2380
2470
  domain: options.domain,
2471
+ service: options.service,
2381
2472
  limit: parseInt(options.limit),
2382
2473
  mode: options.mode,
2383
2474
  });
@@ -2484,6 +2575,242 @@ kbCmd
2484
2575
  process.exit(1);
2485
2576
  }
2486
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
+ });
2487
2814
  // === Ontology 관리 ===
2488
2815
  const ontoCmd = program
2489
2816
  .command("onto")
@@ -2491,11 +2818,15 @@ const ontoCmd = program
2491
2818
  ontoCmd
2492
2819
  .command("list")
2493
2820
  .description("정의된 온톨로지 도메인 목록")
2821
+ .option("--service <name>", "서비스별 필터")
2494
2822
  .option("--format <type>", "출력 형식 (table|json)", "table")
2495
2823
  .action(async (options) => {
2496
2824
  try {
2497
2825
  const pool = (0, database_1.getPool)();
2498
- const domains = await (0, kb_1.ontoList)(pool);
2826
+ let domains = await (0, kb_1.ontoList)(pool);
2827
+ if (options.service) {
2828
+ domains = domains.filter(d => d.service === options.service || d.domain === options.service || d.domain.startsWith(`${options.service}.`));
2829
+ }
2499
2830
  if (options.format === "json") {
2500
2831
  console.log(JSON.stringify(domains, null, 2));
2501
2832
  }
@@ -2505,10 +2836,66 @@ ontoCmd
2505
2836
  console.log(chalk_1.default.yellow(" 온톨로지가 정의되지 않았습니다."));
2506
2837
  }
2507
2838
  else {
2839
+ // Group by service (_global treated as Global)
2840
+ const global = domains.filter(d => !d.service || d.service === '_global');
2841
+ const byService = {};
2508
2842
  for (const d of domains) {
2509
- console.log(chalk_1.default.cyan(` ${d.domain}`) + chalk_1.default.gray(` (v${d.version})`));
2510
- if (d.description)
2511
- console.log(chalk_1.default.gray(` ${d.description}`));
2843
+ if (d.service && d.service !== '_global') {
2844
+ if (!byService[d.service])
2845
+ byService[d.service] = [];
2846
+ byService[d.service].push(d);
2847
+ }
2848
+ }
2849
+ if (global.length > 0) {
2850
+ console.log(chalk_1.default.white.bold(" Global"));
2851
+ for (const d of global) {
2852
+ const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
2853
+ console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + chalk_1.default.gray(` (v${d.version})`));
2854
+ if (d.description)
2855
+ console.log(chalk_1.default.gray(` ${d.description}`));
2856
+ }
2857
+ }
2858
+ for (const [svc, svcDomains] of Object.entries(byService)) {
2859
+ console.log(chalk_1.default.white.bold(`\n Service: ${svc}`));
2860
+ for (const d of svcDomains) {
2861
+ const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
2862
+ console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + chalk_1.default.gray(` (v${d.version})`));
2863
+ if (d.description)
2864
+ console.log(chalk_1.default.gray(` ${d.description}`));
2865
+ }
2866
+ }
2867
+ }
2868
+ console.log();
2869
+ }
2870
+ await (0, database_1.closeConnection)();
2871
+ }
2872
+ catch (err) {
2873
+ console.error(chalk_1.default.red(`조회 실패: ${err}`));
2874
+ await (0, database_1.closeConnection)();
2875
+ process.exit(1);
2876
+ }
2877
+ });
2878
+ ontoCmd
2879
+ .command("types")
2880
+ .description("온톨로지 타입 목록 (구조적 템플릿)")
2881
+ .option("--format <type>", "출력 형식 (table|json)", "table")
2882
+ .action(async (options) => {
2883
+ try {
2884
+ const pool = (0, database_1.getPool)();
2885
+ const types = await (0, kb_1.ontoListTypes)(pool);
2886
+ if (options.format === "json") {
2887
+ console.log(JSON.stringify(types, null, 2));
2888
+ }
2889
+ else {
2890
+ console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 타입\n"));
2891
+ if (types.length === 0) {
2892
+ console.log(chalk_1.default.yellow(" 타입이 정의되지 않았습니다. (016 마이그레이션 실행 필요)"));
2893
+ }
2894
+ else {
2895
+ for (const t of types) {
2896
+ console.log(chalk_1.default.cyan(` ${t.type_key}`) + chalk_1.default.gray(` (v${t.version})`));
2897
+ if (t.description)
2898
+ console.log(chalk_1.default.gray(` ${t.description}`));
2512
2899
  }
2513
2900
  }
2514
2901
  console.log();
@@ -2599,6 +2986,8 @@ ontoCmd
2599
2986
  (0, get_1.registerGetCommands)(program);
2600
2987
  (0, sessions_1.registerSessionsCommands)(program);
2601
2988
  (0, db_1.registerDbCommands)(program);
2989
+ (0, memory_1.registerMemoryCommands)(program);
2990
+ (0, test_1.registerTestCommands)(program);
2602
2991
  // === semo skills — DB 시딩 ===
2603
2992
  /**
2604
2993
  * SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)