@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/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.js +10 -8
- 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.js +2 -2
- package/dist/index.js +333 -103
- package/dist/kb.d.ts +69 -0
- package/dist/kb.js +265 -16
- 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
|
@@ -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.
|
|
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
|
|
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
|
|
1364
|
-
|
|
1365
|
-
|
|
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-
|
|
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
|
-
사용자가 팀 정보를 정정/추가/변경하면, 의사결정이 내려지면 → **반드시
|
|
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
|
-
//
|
|
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
|
|
1814
|
+
## KB 접근 (semo CLI)
|
|
1798
1815
|
|
|
1799
|
-
KB 데이터는 **semo-
|
|
1816
|
+
KB 데이터는 **semo CLI** (\`kb-manager\` 스킬)를 통해 Core DB에서 조회합니다.
|
|
1800
1817
|
|
|
1801
|
-
|
|
|
1802
|
-
|
|
1803
|
-
| \`
|
|
1804
|
-
| \`
|
|
1805
|
-
| \`
|
|
1806
|
-
| \`
|
|
1807
|
-
| \`
|
|
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
|
-
|
|
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
|
*/
|