@team-semicolon/semo-cli 4.2.0 → 4.3.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/memory.d.ts +8 -0
- package/dist/commands/memory.js +297 -0
- package/dist/database.d.ts +1 -0
- package/dist/database.js +2 -1
- package/dist/global-cache.js +13 -0
- package/dist/index.js +171 -12
- package/dist/kb.d.ts +16 -0
- package/dist/kb.js +66 -3
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semo memory sync — L1 (bot workspace) → L2 (KB) 메모리 동기화
|
|
3
|
+
*
|
|
4
|
+
* Bot workspace의 일일 메모리 파일(YYYY-MM-DD.md)을 KB memory 도메인으로 싱크.
|
|
5
|
+
* V1: LLM 요약 없이 raw 저장 (Garden 정책 확정 후 추가 예정)
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
export declare function registerMemoryCommands(program: Command): void;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* semo memory sync — L1 (bot workspace) → L2 (KB) 메모리 동기화
|
|
4
|
+
*
|
|
5
|
+
* Bot workspace의 일일 메모리 파일(YYYY-MM-DD.md)을 KB memory 도메인으로 싱크.
|
|
6
|
+
* V1: LLM 요약 없이 raw 저장 (Garden 정책 확정 후 추가 예정)
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.registerMemoryCommands = registerMemoryCommands;
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
const ora_1 = __importDefault(require("ora"));
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const crypto = __importStar(require("crypto"));
|
|
52
|
+
const database_1 = require("../database");
|
|
53
|
+
const kb_1 = require("../kb");
|
|
54
|
+
// ============================================================
|
|
55
|
+
// Constants
|
|
56
|
+
// ============================================================
|
|
57
|
+
const BOT_IDS = [
|
|
58
|
+
"semiclaw",
|
|
59
|
+
"workclaw",
|
|
60
|
+
"reviewclaw",
|
|
61
|
+
"planclaw",
|
|
62
|
+
"designclaw",
|
|
63
|
+
"infraclaw",
|
|
64
|
+
"growthclaw",
|
|
65
|
+
];
|
|
66
|
+
const MEMORY_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}\.md$/;
|
|
67
|
+
const STATE_DIR = path.join(os.homedir(), ".semo");
|
|
68
|
+
const STATE_FILE = path.join(STATE_DIR, "memory-sync-state.json");
|
|
69
|
+
// ============================================================
|
|
70
|
+
// State Management
|
|
71
|
+
// ============================================================
|
|
72
|
+
function readSyncState() {
|
|
73
|
+
try {
|
|
74
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
75
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8"));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// corrupted state file
|
|
80
|
+
}
|
|
81
|
+
return { synced: {} };
|
|
82
|
+
}
|
|
83
|
+
function writeSyncState(state) {
|
|
84
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
85
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
86
|
+
}
|
|
87
|
+
function contentHash(content) {
|
|
88
|
+
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
89
|
+
}
|
|
90
|
+
// ============================================================
|
|
91
|
+
// File Discovery
|
|
92
|
+
// ============================================================
|
|
93
|
+
function discoverBotMemoryFiles(botId, minAgeDays) {
|
|
94
|
+
const memoryDir = path.join(os.homedir(), `.openclaw-${botId}`, "workspace", "memory");
|
|
95
|
+
if (!fs.existsSync(memoryDir))
|
|
96
|
+
return [];
|
|
97
|
+
const candidates = [];
|
|
98
|
+
const cutoffDate = new Date();
|
|
99
|
+
cutoffDate.setDate(cutoffDate.getDate() - minAgeDays);
|
|
100
|
+
const files = fs.readdirSync(memoryDir);
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
if (!MEMORY_DATE_PATTERN.test(file))
|
|
103
|
+
continue;
|
|
104
|
+
const dateStr = file.replace(".md", "");
|
|
105
|
+
const fileDate = new Date(dateStr + "T23:59:59Z");
|
|
106
|
+
if (fileDate > cutoffDate)
|
|
107
|
+
continue; // Too recent
|
|
108
|
+
const filePath = path.join(memoryDir, file);
|
|
109
|
+
const content = fs.readFileSync(filePath, "utf-8").trim();
|
|
110
|
+
if (!content)
|
|
111
|
+
continue; // Skip empty files
|
|
112
|
+
candidates.push({
|
|
113
|
+
sourceType: "bot",
|
|
114
|
+
sourceId: botId,
|
|
115
|
+
date: dateStr,
|
|
116
|
+
filePath,
|
|
117
|
+
content,
|
|
118
|
+
hash: contentHash(content),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return candidates;
|
|
122
|
+
}
|
|
123
|
+
// ============================================================
|
|
124
|
+
// Sync Logic
|
|
125
|
+
// ============================================================
|
|
126
|
+
async function syncMemories(candidates, state, force, dryRun) {
|
|
127
|
+
let synced = 0;
|
|
128
|
+
let skipped = 0;
|
|
129
|
+
const errors = [];
|
|
130
|
+
if (candidates.length === 0) {
|
|
131
|
+
return { synced, skipped, errors };
|
|
132
|
+
}
|
|
133
|
+
const pool = (0, database_1.getPool)();
|
|
134
|
+
for (const candidate of candidates) {
|
|
135
|
+
const stateKey = `${candidate.sourceId}/${candidate.date}`;
|
|
136
|
+
// Check watermark
|
|
137
|
+
if (!force) {
|
|
138
|
+
const existing = state.synced[stateKey];
|
|
139
|
+
if (existing && existing.hash === candidate.hash) {
|
|
140
|
+
skipped++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (dryRun) {
|
|
145
|
+
console.log(chalk_1.default.gray(` [dry-run] ${stateKey} (${candidate.content.length} chars, hash: ${candidate.hash})`));
|
|
146
|
+
synced++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const domain = "memory";
|
|
151
|
+
const key = stateKey;
|
|
152
|
+
const metadata = {
|
|
153
|
+
source_type: candidate.sourceType,
|
|
154
|
+
source_id: candidate.sourceId,
|
|
155
|
+
date: candidate.date,
|
|
156
|
+
content_hash: candidate.hash,
|
|
157
|
+
original_size: candidate.content.length,
|
|
158
|
+
synced_at: new Date().toISOString(),
|
|
159
|
+
};
|
|
160
|
+
// Generate embedding
|
|
161
|
+
const text = `${key}: ${candidate.content}`;
|
|
162
|
+
const embedding = await (0, kb_1.generateEmbedding)(text);
|
|
163
|
+
const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
|
|
164
|
+
const client = await pool.connect();
|
|
165
|
+
try {
|
|
166
|
+
await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by, embedding)
|
|
167
|
+
VALUES ($1, $2, $3, $4, $5, $6::vector)
|
|
168
|
+
ON CONFLICT (domain, key) DO UPDATE SET
|
|
169
|
+
content = EXCLUDED.content,
|
|
170
|
+
metadata = EXCLUDED.metadata,
|
|
171
|
+
embedding = EXCLUDED.embedding`, [
|
|
172
|
+
domain,
|
|
173
|
+
key,
|
|
174
|
+
candidate.content,
|
|
175
|
+
JSON.stringify(metadata),
|
|
176
|
+
"semo-memory-sync",
|
|
177
|
+
embeddingStr,
|
|
178
|
+
]);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
client.release();
|
|
182
|
+
}
|
|
183
|
+
// Update watermark
|
|
184
|
+
state.synced[stateKey] = {
|
|
185
|
+
hash: candidate.hash,
|
|
186
|
+
syncedAt: new Date().toISOString(),
|
|
187
|
+
};
|
|
188
|
+
synced++;
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
errors.push(`${stateKey}: ${err}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { synced, skipped, errors };
|
|
195
|
+
}
|
|
196
|
+
// ============================================================
|
|
197
|
+
// Command Registration
|
|
198
|
+
// ============================================================
|
|
199
|
+
function registerMemoryCommands(program) {
|
|
200
|
+
const memoryCmd = program
|
|
201
|
+
.command("memory")
|
|
202
|
+
.description("메모리 관리 — L1(bot workspace) → L2(KB) 동기화");
|
|
203
|
+
memoryCmd
|
|
204
|
+
.command("sync")
|
|
205
|
+
.description("봇 워크스페이스 메모리를 KB memory 도메인으로 동기화")
|
|
206
|
+
.option("--source <type>", "소스 타입 (bot | local | all)", "all")
|
|
207
|
+
.option("--bot <id>", "특정 봇만 동기화")
|
|
208
|
+
.option("--days <n>", "N일 이상 경과한 메모리만 동기화", "2")
|
|
209
|
+
.option("--dry-run", "프리뷰만 (실제 동기화 안 함)")
|
|
210
|
+
.option("--force", "워터마크 무시, 전체 재동기화")
|
|
211
|
+
.action(async (options) => {
|
|
212
|
+
const dryRun = !!options.dryRun;
|
|
213
|
+
const force = !!options.force;
|
|
214
|
+
const minAgeDays = parseInt(options.days) || 2;
|
|
215
|
+
const sourceType = options.source;
|
|
216
|
+
const specificBot = options.bot;
|
|
217
|
+
const spinner = (0, ora_1.default)(dryRun ? "동기화 대상 탐색 중..." : "메모리 동기화 중...").start();
|
|
218
|
+
try {
|
|
219
|
+
const state = readSyncState();
|
|
220
|
+
let allCandidates = [];
|
|
221
|
+
// Discover bot memory files
|
|
222
|
+
if (sourceType === "bot" || sourceType === "all") {
|
|
223
|
+
const bots = specificBot ? [specificBot] : BOT_IDS;
|
|
224
|
+
for (const botId of bots) {
|
|
225
|
+
const candidates = discoverBotMemoryFiles(botId, minAgeDays);
|
|
226
|
+
allCandidates.push(...candidates);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
spinner.text = `${allCandidates.length}개 메모리 파일 발견`;
|
|
230
|
+
if (allCandidates.length === 0) {
|
|
231
|
+
spinner.succeed("동기화 대상 메모리 파일 없음");
|
|
232
|
+
await (0, database_1.closeConnection)();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const result = await syncMemories(allCandidates, state, force, dryRun);
|
|
236
|
+
if (!dryRun) {
|
|
237
|
+
writeSyncState(state);
|
|
238
|
+
}
|
|
239
|
+
if (dryRun) {
|
|
240
|
+
spinner.succeed(`[dry-run] ${result.synced}건 동기화 예정, ${result.skipped}건 스킵`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
spinner.succeed(`${result.synced}건 동기화 완료, ${result.skipped}건 스킵 (변경 없음)`);
|
|
244
|
+
}
|
|
245
|
+
if (result.errors.length > 0) {
|
|
246
|
+
console.log(chalk_1.default.yellow(` ⚠️ ${result.errors.length}건 오류:`));
|
|
247
|
+
for (const err of result.errors) {
|
|
248
|
+
console.log(chalk_1.default.red(` ${err}`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
console.log();
|
|
252
|
+
await (0, database_1.closeConnection)();
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
spinner.fail(`메모리 동기화 실패: ${err}`);
|
|
256
|
+
await (0, database_1.closeConnection)();
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
memoryCmd
|
|
261
|
+
.command("status")
|
|
262
|
+
.description("메모리 동기화 상태 확인")
|
|
263
|
+
.option("--bot <id>", "특정 봇만")
|
|
264
|
+
.action(async (options) => {
|
|
265
|
+
const state = readSyncState();
|
|
266
|
+
const specificBot = options.bot;
|
|
267
|
+
console.log(chalk_1.default.cyan.bold("\n📝 메모리 동기화 상태\n"));
|
|
268
|
+
const entries = Object.entries(state.synced);
|
|
269
|
+
if (entries.length === 0) {
|
|
270
|
+
console.log(chalk_1.default.yellow(" 동기화된 메모리 없음"));
|
|
271
|
+
console.log();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Group by source
|
|
275
|
+
const grouped = {};
|
|
276
|
+
for (const [key, val] of entries) {
|
|
277
|
+
const [sourceId, date] = key.split("/");
|
|
278
|
+
if (specificBot && sourceId !== specificBot)
|
|
279
|
+
continue;
|
|
280
|
+
if (!grouped[sourceId])
|
|
281
|
+
grouped[sourceId] = [];
|
|
282
|
+
grouped[sourceId].push({ date, ...val });
|
|
283
|
+
}
|
|
284
|
+
for (const [sourceId, items] of Object.entries(grouped)) {
|
|
285
|
+
console.log(chalk_1.default.white(` ${sourceId}:`));
|
|
286
|
+
const sorted = items.sort((a, b) => b.date.localeCompare(a.date));
|
|
287
|
+
const shown = sorted.slice(0, 10);
|
|
288
|
+
for (const item of shown) {
|
|
289
|
+
console.log(chalk_1.default.gray(` ${item.date} (synced: ${item.syncedAt.split("T")[0]})`));
|
|
290
|
+
}
|
|
291
|
+
if (sorted.length > 10) {
|
|
292
|
+
console.log(chalk_1.default.gray(` ... +${sorted.length - 10} more`));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
console.log();
|
|
296
|
+
});
|
|
297
|
+
}
|
package/dist/database.d.ts
CHANGED
package/dist/database.js
CHANGED
|
@@ -262,7 +262,8 @@ async function getAgents() {
|
|
|
262
262
|
const result = await getPool().query(`
|
|
263
263
|
SELECT id, name, name AS display_name,
|
|
264
264
|
persona_prompt AS content,
|
|
265
|
-
package, is_active, install_order
|
|
265
|
+
package, is_active, install_order,
|
|
266
|
+
metadata
|
|
266
267
|
FROM agent_definitions
|
|
267
268
|
WHERE is_active = true AND office_id IS NULL
|
|
268
269
|
ORDER BY install_order
|
package/dist/global-cache.js
CHANGED
|
@@ -174,6 +174,19 @@ async function syncGlobalCache(claudeDir) {
|
|
|
174
174
|
.join("\n");
|
|
175
175
|
content += `\n\n## 위임 매트릭스\n${delegationLines}\n`;
|
|
176
176
|
}
|
|
177
|
+
// metadata에 model/description이 있으면 YAML frontmatter 주입
|
|
178
|
+
if (agent.metadata && (agent.metadata.model || agent.metadata.description)) {
|
|
179
|
+
const hasFrontmatter = content.trimStart().startsWith('---');
|
|
180
|
+
if (!hasFrontmatter) {
|
|
181
|
+
const fm = ['---'];
|
|
182
|
+
if (agent.metadata.description)
|
|
183
|
+
fm.push(`description: "${agent.metadata.description}"`);
|
|
184
|
+
if (agent.metadata.model)
|
|
185
|
+
fm.push(`model: "${agent.metadata.model}"`);
|
|
186
|
+
fm.push('---', '');
|
|
187
|
+
content = fm.join('\n') + content;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
177
190
|
fs.writeFileSync(path.join(agentFolder, `${agent.name}.md`), content);
|
|
178
191
|
}
|
|
179
192
|
return {
|
package/dist/index.js
CHANGED
|
@@ -63,6 +63,7 @@ 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");
|
|
66
67
|
const global_cache_1 = require("./global-cache");
|
|
67
68
|
const PACKAGE_NAME = "@team-semicolon/semo-cli";
|
|
68
69
|
// package.json에서 버전 동적 로드
|
|
@@ -773,10 +774,16 @@ program
|
|
|
773
774
|
await setupStandardGlobal();
|
|
774
775
|
// 4. Hooks 설치 (프로젝트 무관)
|
|
775
776
|
await setupHooks(false);
|
|
776
|
-
// 5. MCP 설정 (글로벌)
|
|
777
|
+
// 5. MCP 설정 (글로벌 공통 서버)
|
|
777
778
|
if (!options.skipMcp) {
|
|
778
779
|
await setupMCP(os.homedir(), [], options.force || false);
|
|
779
780
|
}
|
|
781
|
+
// 6. semo-kb MCP 유저레벨 등록
|
|
782
|
+
if (!options.skipMcp) {
|
|
783
|
+
await setupSemoKbMcp();
|
|
784
|
+
}
|
|
785
|
+
// 7. 글로벌 CLAUDE.md에 KB-First 규칙 주입
|
|
786
|
+
await injectKbFirstToGlobalClaudeMd();
|
|
780
787
|
await (0, database_1.closeConnection)();
|
|
781
788
|
// 결과 요약
|
|
782
789
|
console.log(chalk_1.default.green.bold("\n✅ SEMO 글로벌 온보딩 완료!\n"));
|
|
@@ -786,6 +793,7 @@ program
|
|
|
786
793
|
console.log(chalk_1.default.gray(" ~/.claude/commands/ 팀 커맨드 (DB 기반)"));
|
|
787
794
|
console.log(chalk_1.default.gray(" ~/.claude/agents/ 팀 에이전트 (DB 기반, dedup)"));
|
|
788
795
|
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)"));
|
|
789
797
|
console.log(chalk_1.default.cyan("\n다음 단계:"));
|
|
790
798
|
console.log(chalk_1.default.gray(" 프로젝트 디렉토리에서 'semo init'을 실행하세요."));
|
|
791
799
|
console.log();
|
|
@@ -1350,6 +1358,96 @@ function registerMCPServer(server) {
|
|
|
1350
1358
|
return { success: false, error: String(error) };
|
|
1351
1359
|
}
|
|
1352
1360
|
}
|
|
1361
|
+
// === 글로벌 CLAUDE.md에 KB-First 규칙 주입 ===
|
|
1362
|
+
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 = `
|
|
1366
|
+
${KB_FIRST_SECTION_MARKER}
|
|
1367
|
+
|
|
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\`
|
|
1378
|
+
|
|
1379
|
+
**금지:** 위 주제를 자체 지식/세션 기억만으로 답변하는 것.
|
|
1380
|
+
KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
|
|
1381
|
+
|
|
1382
|
+
### 쓰기 (Write-Back)
|
|
1383
|
+
사용자가 팀 정보를 정정/추가/변경하면, 의사결정이 내려지면 → **반드시 kb_upsert로 KB에 즉시 기록.**
|
|
1384
|
+
**금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
|
|
1385
|
+
`;
|
|
1386
|
+
if (fs.existsSync(globalClaudeMd)) {
|
|
1387
|
+
const content = fs.readFileSync(globalClaudeMd, "utf-8");
|
|
1388
|
+
if (content.includes(KB_FIRST_SECTION_MARKER)) {
|
|
1389
|
+
// 기존 섹션 교체
|
|
1390
|
+
const regex = new RegExp(`\\n${KB_FIRST_SECTION_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?(?=\\n## |$)`, "m");
|
|
1391
|
+
const updated = content.replace(regex, kbFirstBlock);
|
|
1392
|
+
fs.writeFileSync(globalClaudeMd, updated);
|
|
1393
|
+
console.log(chalk_1.default.gray(" ~/.claude/CLAUDE.md KB-First 규칙 업데이트됨"));
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
// 끝에 추가
|
|
1397
|
+
fs.writeFileSync(globalClaudeMd, content.trimEnd() + "\n" + kbFirstBlock);
|
|
1398
|
+
console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md에 KB-First 규칙 추가됨"));
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
else {
|
|
1402
|
+
// 파일 없으면 생성
|
|
1403
|
+
fs.writeFileSync(globalClaudeMd, kbFirstBlock.trim() + "\n");
|
|
1404
|
+
console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md 생성됨 (KB-First 규칙)"));
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
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
|
+
}
|
|
1353
1451
|
// === MCP 설정 ===
|
|
1354
1452
|
async function setupMCP(cwd, _extensions, force) {
|
|
1355
1453
|
console.log(chalk_1.default.cyan("\n🔧 Black Box 설정 (MCP Server)"));
|
|
@@ -1366,14 +1464,10 @@ async function setupMCP(cwd, _extensions, force) {
|
|
|
1366
1464
|
const settings = {
|
|
1367
1465
|
mcpServers: {},
|
|
1368
1466
|
};
|
|
1369
|
-
//
|
|
1370
|
-
// 공통 서버(context7 등)
|
|
1371
|
-
settings.mcpServers["semo-kb"] = {
|
|
1372
|
-
command: "node",
|
|
1373
|
-
args: ["packages/mcp-kb/dist/index.js"],
|
|
1374
|
-
};
|
|
1467
|
+
// semo-kb는 유저레벨에서 등록 (semo onboarding)하므로 프로젝트 settings에 쓰지 않음
|
|
1468
|
+
// 공통 서버(context7 등)도 유저레벨에 등록하므로 프로젝트 settings에 쓰지 않음
|
|
1375
1469
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
1376
|
-
console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨
|
|
1470
|
+
console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨"));
|
|
1377
1471
|
// Claude Code에 MCP 서버 등록 시도 (공통 서버는 유저레벨로)
|
|
1378
1472
|
console.log(chalk_1.default.cyan("\n🔌 Claude Code에 MCP 서버 등록 중..."));
|
|
1379
1473
|
const allServers = [...BASE_MCP_SERVERS];
|
|
@@ -2331,6 +2425,7 @@ kbCmd
|
|
|
2331
2425
|
.command("list")
|
|
2332
2426
|
.description("KB 항목 목록 조회")
|
|
2333
2427
|
.option("--domain <name>", "도메인 필터")
|
|
2428
|
+
.option("--service <name>", "서비스(프로젝트) 필터 — 해당 서비스의 모든 도메인 항목 반환")
|
|
2334
2429
|
.option("--limit <n>", "최대 항목 수", "50")
|
|
2335
2430
|
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
2336
2431
|
.action(async (options) => {
|
|
@@ -2338,6 +2433,7 @@ kbCmd
|
|
|
2338
2433
|
const pool = (0, database_1.getPool)();
|
|
2339
2434
|
const entries = await (0, kb_1.kbList)(pool, {
|
|
2340
2435
|
domain: options.domain,
|
|
2436
|
+
service: options.service,
|
|
2341
2437
|
limit: parseInt(options.limit),
|
|
2342
2438
|
});
|
|
2343
2439
|
if (options.format === "json") {
|
|
@@ -2370,6 +2466,7 @@ kbCmd
|
|
|
2370
2466
|
.command("search <query>")
|
|
2371
2467
|
.description("KB 검색 (시맨틱 + 텍스트 하이브리드)")
|
|
2372
2468
|
.option("--domain <name>", "도메인 필터")
|
|
2469
|
+
.option("--service <name>", "서비스(프로젝트) 필터")
|
|
2373
2470
|
.option("--limit <n>", "최대 결과 수", "10")
|
|
2374
2471
|
.option("--mode <type>", "검색 모드 (hybrid|semantic|text)", "hybrid")
|
|
2375
2472
|
.action(async (query, options) => {
|
|
@@ -2378,6 +2475,7 @@ kbCmd
|
|
|
2378
2475
|
const pool = (0, database_1.getPool)();
|
|
2379
2476
|
const results = await (0, kb_1.kbSearch)(pool, query, {
|
|
2380
2477
|
domain: options.domain,
|
|
2478
|
+
service: options.service,
|
|
2381
2479
|
limit: parseInt(options.limit),
|
|
2382
2480
|
mode: options.mode,
|
|
2383
2481
|
});
|
|
@@ -2491,11 +2589,15 @@ const ontoCmd = program
|
|
|
2491
2589
|
ontoCmd
|
|
2492
2590
|
.command("list")
|
|
2493
2591
|
.description("정의된 온톨로지 도메인 목록")
|
|
2592
|
+
.option("--service <name>", "서비스별 필터")
|
|
2494
2593
|
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
2495
2594
|
.action(async (options) => {
|
|
2496
2595
|
try {
|
|
2497
2596
|
const pool = (0, database_1.getPool)();
|
|
2498
|
-
|
|
2597
|
+
let domains = await (0, kb_1.ontoList)(pool);
|
|
2598
|
+
if (options.service) {
|
|
2599
|
+
domains = domains.filter(d => d.service === options.service || d.domain === options.service || d.domain.startsWith(`${options.service}.`));
|
|
2600
|
+
}
|
|
2499
2601
|
if (options.format === "json") {
|
|
2500
2602
|
console.log(JSON.stringify(domains, null, 2));
|
|
2501
2603
|
}
|
|
@@ -2505,10 +2607,66 @@ ontoCmd
|
|
|
2505
2607
|
console.log(chalk_1.default.yellow(" 온톨로지가 정의되지 않았습니다."));
|
|
2506
2608
|
}
|
|
2507
2609
|
else {
|
|
2610
|
+
// Group by service
|
|
2611
|
+
const global = domains.filter(d => !d.service);
|
|
2612
|
+
const byService = {};
|
|
2508
2613
|
for (const d of domains) {
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2614
|
+
if (d.service) {
|
|
2615
|
+
if (!byService[d.service])
|
|
2616
|
+
byService[d.service] = [];
|
|
2617
|
+
byService[d.service].push(d);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
if (global.length > 0) {
|
|
2621
|
+
console.log(chalk_1.default.white.bold(" Global"));
|
|
2622
|
+
for (const d of global) {
|
|
2623
|
+
const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
|
|
2624
|
+
console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + chalk_1.default.gray(` (v${d.version})`));
|
|
2625
|
+
if (d.description)
|
|
2626
|
+
console.log(chalk_1.default.gray(` ${d.description}`));
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
for (const [svc, svcDomains] of Object.entries(byService)) {
|
|
2630
|
+
console.log(chalk_1.default.white.bold(`\n Service: ${svc}`));
|
|
2631
|
+
for (const d of svcDomains) {
|
|
2632
|
+
const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
|
|
2633
|
+
console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + chalk_1.default.gray(` (v${d.version})`));
|
|
2634
|
+
if (d.description)
|
|
2635
|
+
console.log(chalk_1.default.gray(` ${d.description}`));
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
console.log();
|
|
2640
|
+
}
|
|
2641
|
+
await (0, database_1.closeConnection)();
|
|
2642
|
+
}
|
|
2643
|
+
catch (err) {
|
|
2644
|
+
console.error(chalk_1.default.red(`조회 실패: ${err}`));
|
|
2645
|
+
await (0, database_1.closeConnection)();
|
|
2646
|
+
process.exit(1);
|
|
2647
|
+
}
|
|
2648
|
+
});
|
|
2649
|
+
ontoCmd
|
|
2650
|
+
.command("types")
|
|
2651
|
+
.description("온톨로지 타입 목록 (구조적 템플릿)")
|
|
2652
|
+
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
2653
|
+
.action(async (options) => {
|
|
2654
|
+
try {
|
|
2655
|
+
const pool = (0, database_1.getPool)();
|
|
2656
|
+
const types = await (0, kb_1.ontoListTypes)(pool);
|
|
2657
|
+
if (options.format === "json") {
|
|
2658
|
+
console.log(JSON.stringify(types, null, 2));
|
|
2659
|
+
}
|
|
2660
|
+
else {
|
|
2661
|
+
console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 타입\n"));
|
|
2662
|
+
if (types.length === 0) {
|
|
2663
|
+
console.log(chalk_1.default.yellow(" 타입이 정의되지 않았습니다. (016 마이그레이션 실행 필요)"));
|
|
2664
|
+
}
|
|
2665
|
+
else {
|
|
2666
|
+
for (const t of types) {
|
|
2667
|
+
console.log(chalk_1.default.cyan(` ${t.type_key}`) + chalk_1.default.gray(` (v${t.version})`));
|
|
2668
|
+
if (t.description)
|
|
2669
|
+
console.log(chalk_1.default.gray(` ${t.description}`));
|
|
2512
2670
|
}
|
|
2513
2671
|
}
|
|
2514
2672
|
console.log();
|
|
@@ -2599,6 +2757,7 @@ ontoCmd
|
|
|
2599
2757
|
(0, get_1.registerGetCommands)(program);
|
|
2600
2758
|
(0, sessions_1.registerSessionsCommands)(program);
|
|
2601
2759
|
(0, db_1.registerDbCommands)(program);
|
|
2760
|
+
(0, memory_1.registerMemoryCommands)(program);
|
|
2602
2761
|
// === semo skills — DB 시딩 ===
|
|
2603
2762
|
/**
|
|
2604
2763
|
* SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
|
package/dist/kb.d.ts
CHANGED
|
@@ -32,8 +32,18 @@ export interface OntologyDomain {
|
|
|
32
32
|
schema: Record<string, unknown>;
|
|
33
33
|
description: string | null;
|
|
34
34
|
version: number;
|
|
35
|
+
service?: string | null;
|
|
36
|
+
entity_type?: string | null;
|
|
37
|
+
parent?: string | null;
|
|
38
|
+
tags?: string[];
|
|
35
39
|
updated_at?: string;
|
|
36
40
|
}
|
|
41
|
+
export interface OntologyType {
|
|
42
|
+
type_key: string;
|
|
43
|
+
schema: Record<string, unknown>;
|
|
44
|
+
description: string | null;
|
|
45
|
+
version: number;
|
|
46
|
+
}
|
|
37
47
|
export interface KBStatusInfo {
|
|
38
48
|
shared: {
|
|
39
49
|
total: number;
|
|
@@ -79,6 +89,7 @@ export declare function kbStatus(pool: Pool): Promise<KBStatusInfo>;
|
|
|
79
89
|
*/
|
|
80
90
|
export declare function kbList(pool: Pool, options: {
|
|
81
91
|
domain?: string;
|
|
92
|
+
service?: string;
|
|
82
93
|
limit?: number;
|
|
83
94
|
offset?: number;
|
|
84
95
|
}): Promise<KBEntry[]>;
|
|
@@ -87,6 +98,7 @@ export declare function kbList(pool: Pool, options: {
|
|
|
87
98
|
*/
|
|
88
99
|
export declare function kbSearch(pool: Pool, query: string, options: {
|
|
89
100
|
domain?: string;
|
|
101
|
+
service?: string;
|
|
90
102
|
limit?: number;
|
|
91
103
|
mode?: "semantic" | "text" | "hybrid";
|
|
92
104
|
}): Promise<KBEntry[]>;
|
|
@@ -98,6 +110,10 @@ export declare function ontoList(pool: Pool): Promise<OntologyDomain[]>;
|
|
|
98
110
|
* Show ontology detail for a domain
|
|
99
111
|
*/
|
|
100
112
|
export declare function ontoShow(pool: Pool, domain: string): Promise<OntologyDomain | null>;
|
|
113
|
+
/**
|
|
114
|
+
* List all ontology types (structural templates)
|
|
115
|
+
*/
|
|
116
|
+
export declare function ontoListTypes(pool: Pool): Promise<OntologyType[]>;
|
|
101
117
|
/**
|
|
102
118
|
* Validate KB entries against ontology schema (basic JSON Schema validation)
|
|
103
119
|
*/
|
package/dist/kb.js
CHANGED
|
@@ -51,6 +51,7 @@ exports.kbList = kbList;
|
|
|
51
51
|
exports.kbSearch = kbSearch;
|
|
52
52
|
exports.ontoList = ontoList;
|
|
53
53
|
exports.ontoShow = ontoShow;
|
|
54
|
+
exports.ontoListTypes = ontoListTypes;
|
|
54
55
|
exports.ontoValidate = ontoValidate;
|
|
55
56
|
exports.kbDigest = kbDigest;
|
|
56
57
|
exports.ontoPullToLocal = ontoPullToLocal;
|
|
@@ -313,6 +314,14 @@ async function kbList(pool, options) {
|
|
|
313
314
|
query += ` WHERE domain = $${paramIdx++}`;
|
|
314
315
|
params.push(options.domain);
|
|
315
316
|
}
|
|
317
|
+
else if (options.service) {
|
|
318
|
+
// Resolve service to domain list: service name itself + dot-notation domains
|
|
319
|
+
const serviceDomains = await resolveServiceDomainsLocal(client, options.service);
|
|
320
|
+
if (serviceDomains.length > 0) {
|
|
321
|
+
query += ` WHERE domain = ANY($${paramIdx++})`;
|
|
322
|
+
params.push(serviceDomains);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
316
325
|
query += ` ORDER BY domain, key LIMIT $${paramIdx++} OFFSET $${paramIdx++}`;
|
|
317
326
|
params.push(limit, offset);
|
|
318
327
|
const result = await client.query(query, params);
|
|
@@ -329,6 +338,11 @@ async function kbSearch(pool, query, options) {
|
|
|
329
338
|
const client = await pool.connect();
|
|
330
339
|
const limit = options.limit || 10;
|
|
331
340
|
const mode = options.mode || "hybrid";
|
|
341
|
+
// Resolve service → domain list for filtering
|
|
342
|
+
let serviceDomains = null;
|
|
343
|
+
if (options.service && !options.domain) {
|
|
344
|
+
serviceDomains = await resolveServiceDomainsLocal(client, options.service);
|
|
345
|
+
}
|
|
332
346
|
try {
|
|
333
347
|
let results = [];
|
|
334
348
|
// Try semantic search first (if mode allows and embedding API available)
|
|
@@ -349,6 +363,10 @@ async function kbSearch(pool, query, options) {
|
|
|
349
363
|
sql += ` AND domain = $${paramIdx++}`;
|
|
350
364
|
params.push(options.domain);
|
|
351
365
|
}
|
|
366
|
+
else if (serviceDomains && serviceDomains.length > 0) {
|
|
367
|
+
sql += ` AND domain = ANY($${paramIdx++})`;
|
|
368
|
+
params.push(serviceDomains);
|
|
369
|
+
}
|
|
352
370
|
sql += ` ORDER BY embedding <=> $1::vector LIMIT $${paramIdx++}`;
|
|
353
371
|
params.push(limit);
|
|
354
372
|
const sharedResult = await client.query(sql, params);
|
|
@@ -390,6 +408,10 @@ async function kbSearch(pool, query, options) {
|
|
|
390
408
|
textSql += ` AND domain = $${tIdx++}`;
|
|
391
409
|
textParams.push(options.domain);
|
|
392
410
|
}
|
|
411
|
+
else if (serviceDomains && serviceDomains.length > 0) {
|
|
412
|
+
textSql += ` AND domain = ANY($${tIdx++})`;
|
|
413
|
+
textParams.push(serviceDomains);
|
|
414
|
+
}
|
|
393
415
|
textSql += ` ORDER BY score DESC, updated_at DESC LIMIT $${tIdx++}`;
|
|
394
416
|
textParams.push(limit);
|
|
395
417
|
const textResult = await client.query(textSql, textParams);
|
|
@@ -447,8 +469,10 @@ async function ontoList(pool) {
|
|
|
447
469
|
const client = await pool.connect();
|
|
448
470
|
try {
|
|
449
471
|
const result = await client.query(`
|
|
450
|
-
SELECT domain, schema, description, version,
|
|
451
|
-
|
|
472
|
+
SELECT domain, schema, description, version,
|
|
473
|
+
service, entity_type, parent, tags,
|
|
474
|
+
updated_at::text
|
|
475
|
+
FROM semo.ontology ORDER BY service NULLS FIRST, domain
|
|
452
476
|
`);
|
|
453
477
|
return result.rows;
|
|
454
478
|
}
|
|
@@ -462,13 +486,52 @@ async function ontoList(pool) {
|
|
|
462
486
|
async function ontoShow(pool, domain) {
|
|
463
487
|
const client = await pool.connect();
|
|
464
488
|
try {
|
|
465
|
-
const result = await client.query(`SELECT domain, schema, description, version,
|
|
489
|
+
const result = await client.query(`SELECT domain, schema, description, version,
|
|
490
|
+
service, entity_type, parent, tags,
|
|
491
|
+
updated_at::text
|
|
492
|
+
FROM semo.ontology WHERE domain = $1`, [domain]);
|
|
466
493
|
return result.rows[0] || null;
|
|
467
494
|
}
|
|
468
495
|
finally {
|
|
469
496
|
client.release();
|
|
470
497
|
}
|
|
471
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* List all ontology types (structural templates)
|
|
501
|
+
*/
|
|
502
|
+
async function ontoListTypes(pool) {
|
|
503
|
+
const client = await pool.connect();
|
|
504
|
+
try {
|
|
505
|
+
const result = await client.query(`
|
|
506
|
+
SELECT type_key, schema, description, version
|
|
507
|
+
FROM semo.ontology_types ORDER BY type_key
|
|
508
|
+
`);
|
|
509
|
+
return result.rows;
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return []; // Table may not exist yet (pre-016 migration)
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
client.release();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Resolve a service name to its associated domain list.
|
|
520
|
+
* Uses ontology.service column + dot-notation domain detection.
|
|
521
|
+
*/
|
|
522
|
+
async function resolveServiceDomainsLocal(client, service) {
|
|
523
|
+
try {
|
|
524
|
+
const result = await client.query(`SELECT domain FROM semo.ontology WHERE service = $1
|
|
525
|
+
UNION
|
|
526
|
+
SELECT domain FROM semo.ontology WHERE domain LIKE $2
|
|
527
|
+
UNION
|
|
528
|
+
SELECT domain FROM semo.ontology WHERE domain = $1`, [service, `${service}.%`]);
|
|
529
|
+
return result.rows.map((r) => r.domain);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return [];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
472
535
|
/**
|
|
473
536
|
* Validate KB entries against ontology schema (basic JSON Schema validation)
|
|
474
537
|
*/
|