@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/commands/audit.d.ts +12 -4
- package/dist/commands/audit.js +219 -27
- package/dist/commands/bots.js +60 -37
- package/dist/commands/context.d.ts +2 -2
- package/dist/commands/context.js +11 -30
- package/dist/commands/get.js +31 -17
- package/dist/commands/memory.d.ts +8 -0
- package/dist/commands/memory.js +299 -0
- package/dist/commands/skill-sync.js +1 -1
- package/dist/commands/test.d.ts +11 -0
- package/dist/commands/test.js +520 -0
- package/dist/database.d.ts +1 -0
- package/dist/database.js +4 -3
- package/dist/global-cache.js +13 -0
- package/dist/index.js +434 -45
- package/dist/kb.d.ts +85 -0
- package/dist/kb.js +331 -19
- package/dist/slack-notify.d.ts +8 -0
- package/dist/slack-notify.js +45 -0
- package/dist/test-runners/workspace-audit.d.ts +17 -0
- package/dist/test-runners/workspace-audit.js +366 -0
- package/package.json +1 -1
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 생성됨
|
|
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
|
|
1814
|
+
## KB 접근 (semo CLI)
|
|
1704
1815
|
|
|
1705
|
-
KB 데이터는 **semo-
|
|
1816
|
+
KB 데이터는 **semo CLI** (\`kb-manager\` 스킬)를 통해 Core DB에서 조회합니다.
|
|
1706
1817
|
|
|
1707
|
-
|
|
|
1708
|
-
|
|
1709
|
-
| \`
|
|
1710
|
-
| \`
|
|
1711
|
-
| \`
|
|
1712
|
-
| \`
|
|
1713
|
-
| \`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
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 기반)
|