@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.
@@ -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
+ }
@@ -43,6 +43,7 @@ export interface Agent {
43
43
  package: string;
44
44
  is_active: boolean;
45
45
  install_order: number;
46
+ metadata?: Record<string, unknown>;
46
47
  }
47
48
  export interface Package {
48
49
  id: string;
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
@@ -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
- // settings.json에는 프로젝트 전용 semo-kb만 기록
1370
- // 공통 서버(context7 등) 유저레벨에 등록하므로 프로젝트 settings에 쓰지 않음
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 생성됨 (semo-kb MCP 설정)"));
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
- const domains = await (0, kb_1.ontoList)(pool);
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
- console.log(chalk_1.default.cyan(` ${d.domain}`) + chalk_1.default.gray(` (v${d.version})`));
2510
- if (d.description)
2511
- console.log(chalk_1.default.gray(` ${d.description}`));
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, updated_at::text
451
- FROM semo.ontology ORDER BY domain
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, updated_at::text FROM semo.ontology WHERE domain = $1`, [domain]);
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
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {