@team-semicolon/semo-cli 4.12.0 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -66,18 +66,21 @@ const db_1 = require("./commands/db");
66
66
  const memory_1 = require("./commands/memory");
67
67
  const test_1 = require("./commands/test");
68
68
  const commitments_1 = require("./commands/commitments");
69
+ const service_1 = require("./commands/service");
70
+ const harness_1 = require("./commands/harness");
71
+ const incubator_1 = require("./commands/incubator");
69
72
  const global_cache_1 = require("./global-cache");
70
73
  const semo_workspace_1 = require("./semo-workspace");
71
- const PACKAGE_NAME = "@team-semicolon/semo-cli";
74
+ const PACKAGE_NAME = '@team-semicolon/semo-cli';
72
75
  // package.json에서 버전 동적 로드
73
76
  function getCliVersion() {
74
77
  try {
75
- const pkgPath = path.join(__dirname, "..", "package.json");
76
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
77
- return pkg.version || "unknown";
78
+ const pkgPath = path.join(__dirname, '..', 'package.json');
79
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
80
+ return pkg.version || 'unknown';
78
81
  }
79
82
  catch {
80
- return "unknown";
83
+ return 'unknown';
81
84
  }
82
85
  }
83
86
  const VERSION = getCliVersion();
@@ -88,8 +91,8 @@ const VERSION = getCliVersion();
88
91
  async function getLatestVersion() {
89
92
  try {
90
93
  const result = (0, child_process_1.execSync)(`npm view ${PACKAGE_NAME} version`, {
91
- stdio: "pipe",
92
- encoding: "utf-8",
94
+ stdio: 'pipe',
95
+ encoding: 'utf-8',
93
96
  timeout: 10000, // 10초 타임아웃
94
97
  });
95
98
  return result.trim();
@@ -104,9 +107,9 @@ async function getLatestVersion() {
104
107
  */
105
108
  function isVersionLower(current, latest) {
106
109
  // alpha, beta 등 pre-release 태그 제거 후 비교
107
- const cleanVersion = (v) => v.replace(/-.*$/, "");
108
- const currentParts = cleanVersion(current).split(".").map(Number);
109
- const latestParts = cleanVersion(latest).split(".").map(Number);
110
+ const cleanVersion = (v) => v.replace(/-.*$/, '');
111
+ const currentParts = cleanVersion(current).split('.').map(Number);
112
+ const latestParts = cleanVersion(latest).split('.').map(Number);
110
113
  for (let i = 0; i < 3; i++) {
111
114
  const c = currentParts[i] || 0;
112
115
  const l = latestParts[i] || 0;
@@ -117,8 +120,8 @@ function isVersionLower(current, latest) {
117
120
  }
118
121
  // 숫자가 같으면 pre-release 여부 확인
119
122
  // current가 pre-release이고 latest가 정식이면 낮은 버전
120
- const currentIsPrerelease = current.includes("-");
121
- const latestIsPrerelease = latest.includes("-");
123
+ const currentIsPrerelease = current.includes('-');
124
+ const latestIsPrerelease = latest.includes('-');
122
125
  if (currentIsPrerelease && !latestIsPrerelease)
123
126
  return true;
124
127
  return false;
@@ -127,8 +130,8 @@ function isVersionLower(current, latest) {
127
130
  * init/update 시작 시 CLI 버전 비교 결과 출력
128
131
  */
129
132
  async function showVersionComparison() {
130
- console.log(chalk_1.default.cyan("📊 버전 확인\n"));
131
- const spinner = (0, ora_1.default)(" 버전 정보 조회 중...").start();
133
+ console.log(chalk_1.default.cyan('📊 버전 확인\n'));
134
+ const spinner = (0, ora_1.default)(' 버전 정보 조회 중...').start();
132
135
  try {
133
136
  const currentCliVersion = VERSION;
134
137
  const latestCliVersion = await getLatestVersion();
@@ -142,22 +145,22 @@ async function showVersionComparison() {
142
145
  console.log(chalk_1.default.cyan(` npm install -g ${PACKAGE_NAME}@latest`));
143
146
  }
144
147
  else {
145
- console.log(chalk_1.default.green("\n ✓ 최신 버전입니다"));
148
+ console.log(chalk_1.default.green('\n ✓ 최신 버전입니다'));
146
149
  }
147
- console.log("");
150
+ console.log('');
148
151
  }
149
152
  catch (error) {
150
- spinner.fail(" 버전 정보 조회 실패");
153
+ spinner.fail(' 버전 정보 조회 실패');
151
154
  console.log(chalk_1.default.gray(` ${error}`));
152
- console.log("");
155
+ console.log('');
153
156
  }
154
157
  }
155
158
  // === Windows 지원 유틸리티 ===
156
159
  // Git Bash, WSL 등에서도 Windows로 인식하도록 확장
157
- const isWindows = os.platform() === "win32" ||
158
- process.env.OSTYPE?.includes("msys") ||
159
- process.env.OSTYPE?.includes("cygwin") ||
160
- process.env.TERM_PROGRAM === "mintty";
160
+ const isWindows = os.platform() === 'win32' ||
161
+ process.env.OSTYPE?.includes('msys') ||
162
+ process.env.OSTYPE?.includes('cygwin') ||
163
+ process.env.TERM_PROGRAM === 'mintty';
161
164
  /**
162
165
  * 레거시 SEMO 환경을 감지합니다.
163
166
  * 레거시: 프로젝트 루트에 semo-core/ 가 직접 있는 경우
@@ -166,7 +169,7 @@ const isWindows = os.platform() === "win32" ||
166
169
  function detectLegacyEnvironment(cwd) {
167
170
  const legacyPaths = [];
168
171
  // 루트에 직접 있는 레거시 디렉토리 확인
169
- const legacyDirs = ["semo-core", "sax-core", "sax-skills"];
172
+ const legacyDirs = ['semo-core', 'sax-core', 'sax-skills'];
170
173
  for (const dir of legacyDirs) {
171
174
  const dirPath = path.join(cwd, dir);
172
175
  if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
@@ -174,7 +177,7 @@ function detectLegacyEnvironment(cwd) {
174
177
  }
175
178
  }
176
179
  // .claude/ 내부의 레거시 구조 확인
177
- const claudeDir = path.join(cwd, ".claude");
180
+ const claudeDir = path.join(cwd, '.claude');
178
181
  if (fs.existsSync(claudeDir)) {
179
182
  // 심볼릭 링크가 레거시 경로를 가리키는지 확인
180
183
  const checkLegacyLink = (linkName) => {
@@ -192,26 +195,26 @@ function detectLegacyEnvironment(cwd) {
192
195
  }
193
196
  }
194
197
  };
195
- checkLegacyLink("agents");
196
- checkLegacyLink("skills");
197
- checkLegacyLink("commands");
198
+ checkLegacyLink('agents');
199
+ checkLegacyLink('skills');
200
+ checkLegacyLink('commands');
198
201
  }
199
202
  return {
200
203
  hasLegacy: legacyPaths.length > 0,
201
204
  legacyPaths,
202
- hasSemoSystem: fs.existsSync(path.join(cwd, "semo-system")),
205
+ hasSemoSystem: fs.existsSync(path.join(cwd, 'semo-system')),
203
206
  };
204
207
  }
205
208
  // (migrateLegacyEnvironment / removeRecursive removed — semo-system migration no longer needed)
206
209
  const program = new commander_1.Command();
207
210
  program
208
- .name("semo")
209
- .description("SEMO CLI - AI Agent Orchestration Framework")
210
- .version(VERSION, "-V, --version-simple", "버전 번호만 출력");
211
+ .name('semo')
212
+ .description('SEMO CLI - AI Agent Orchestration Framework')
213
+ .version(VERSION, '-V, --version-simple', '버전 번호만 출력');
211
214
  // === version 명령어 (상세 버전 정보) ===
212
215
  program
213
- .command("version")
214
- .description("버전 정보 및 업데이트 확인")
216
+ .command('version')
217
+ .description('버전 정보 및 업데이트 확인')
215
218
  .action(async () => {
216
219
  await showVersionInfo();
217
220
  });
@@ -219,7 +222,7 @@ program
219
222
  * 상세 버전 정보 표시 및 업데이트 확인
220
223
  */
221
224
  async function showVersionInfo() {
222
- console.log(chalk_1.default.cyan.bold("\n📦 SEMO 버전 정보\n"));
225
+ console.log(chalk_1.default.cyan.bold('\n📦 SEMO 버전 정보\n'));
223
226
  const latestCliVersion = await getLatestVersion();
224
227
  console.log(chalk_1.default.white(` semo-cli: ${chalk_1.default.green.bold(VERSION)}`));
225
228
  if (latestCliVersion) {
@@ -227,12 +230,12 @@ async function showVersionInfo() {
227
230
  }
228
231
  if (latestCliVersion && isVersionLower(VERSION, latestCliVersion)) {
229
232
  console.log();
230
- console.log(chalk_1.default.yellow.bold(" ⚠️ CLI 업데이트 가능"));
233
+ console.log(chalk_1.default.yellow.bold(' ⚠️ CLI 업데이트 가능'));
231
234
  console.log(chalk_1.default.cyan(` npm install -g ${PACKAGE_NAME}@latest`));
232
235
  }
233
236
  else {
234
237
  console.log();
235
- console.log(chalk_1.default.green(" ✓ 최신 버전"));
238
+ console.log(chalk_1.default.green(' ✓ 최신 버전'));
236
239
  }
237
240
  console.log();
238
241
  }
@@ -243,8 +246,8 @@ async function confirmOverwrite(itemName, itemPath) {
243
246
  }
244
247
  const { shouldOverwrite } = await inquirer_1.default.prompt([
245
248
  {
246
- type: "confirm",
247
- name: "shouldOverwrite",
249
+ type: 'confirm',
250
+ name: 'shouldOverwrite',
248
251
  message: chalk_1.default.yellow(`${itemName} 이미 존재합니다. SEMO 기준으로 덮어쓰시겠습니까?`),
249
252
  default: true,
250
253
  },
@@ -254,34 +257,36 @@ async function confirmOverwrite(itemName, itemPath) {
254
257
  function checkRequiredTools() {
255
258
  const tools = [
256
259
  {
257
- name: "GitHub CLI (gh)",
260
+ name: 'GitHub CLI (gh)',
258
261
  installed: false,
259
- installCmd: isWindows ? "winget install GitHub.cli" : "brew install gh",
260
- description: "GitHub API 연동 (이슈, PR, 배포)",
262
+ installCmd: isWindows ? 'winget install GitHub.cli' : 'brew install gh',
263
+ description: 'GitHub API 연동 (이슈, PR, 배포)',
261
264
  },
262
265
  {
263
- name: "Supabase CLI",
266
+ name: 'Supabase CLI',
264
267
  installed: false,
265
- installCmd: isWindows ? "winget install Supabase.CLI" : "brew install supabase/tap/supabase",
266
- description: "Supabase 데이터베이스 연동",
267
- windowsAltCmds: isWindows ? [
268
- "scoop bucket add supabase https://github.com/supabase/scoop-bucket.git && scoop install supabase",
269
- "choco install supabase"
270
- ] : undefined,
268
+ installCmd: isWindows ? 'winget install Supabase.CLI' : 'brew install supabase/tap/supabase',
269
+ description: 'Supabase 데이터베이스 연동',
270
+ windowsAltCmds: isWindows
271
+ ? [
272
+ 'scoop bucket add supabase https://github.com/supabase/scoop-bucket.git && scoop install supabase',
273
+ 'choco install supabase',
274
+ ]
275
+ : undefined,
271
276
  },
272
277
  ];
273
278
  // GitHub CLI 확인
274
279
  try {
275
- const ghVersion = (0, child_process_1.execSync)("gh --version", { stdio: "pipe", encoding: "utf-8" });
280
+ const ghVersion = (0, child_process_1.execSync)('gh --version', { stdio: 'pipe', encoding: 'utf-8' });
276
281
  tools[0].installed = true;
277
- tools[0].version = ghVersion.split("\n")[0].replace("gh version ", "").trim();
282
+ tools[0].version = ghVersion.split('\n')[0].replace('gh version ', '').trim();
278
283
  }
279
284
  catch {
280
285
  // gh not installed
281
286
  }
282
287
  // Supabase CLI 확인
283
288
  try {
284
- const supabaseVersion = (0, child_process_1.execSync)("supabase --version", { stdio: "pipe", encoding: "utf-8" });
289
+ const supabaseVersion = (0, child_process_1.execSync)('supabase --version', { stdio: 'pipe', encoding: 'utf-8' });
285
290
  tools[1].installed = true;
286
291
  tools[1].version = supabaseVersion.trim();
287
292
  }
@@ -291,12 +296,12 @@ function checkRequiredTools() {
291
296
  return tools;
292
297
  }
293
298
  async function showToolsStatus() {
294
- console.log(chalk_1.default.cyan("\n🔍 필수 도구 확인"));
299
+ console.log(chalk_1.default.cyan('\n🔍 필수 도구 확인'));
295
300
  const tools = checkRequiredTools();
296
- const missingTools = tools.filter(t => !t.installed);
301
+ const missingTools = tools.filter((t) => !t.installed);
297
302
  for (const tool of tools) {
298
303
  if (tool.installed) {
299
- console.log(chalk_1.default.green(` ✓ ${tool.name} ${tool.version ? `(${tool.version})` : ""}`));
304
+ console.log(chalk_1.default.green(` ✓ ${tool.name} ${tool.version ? `(${tool.version})` : ''}`));
300
305
  }
301
306
  else {
302
307
  console.log(chalk_1.default.yellow(` ✗ ${tool.name} - 미설치`));
@@ -304,13 +309,13 @@ async function showToolsStatus() {
304
309
  }
305
310
  }
306
311
  if (missingTools.length > 0) {
307
- console.log(chalk_1.default.yellow("\n⚠ 일부 도구가 설치되어 있지 않습니다."));
308
- console.log(chalk_1.default.gray(" SEMO의 일부 기능이 제한될 수 있습니다.\n"));
309
- console.log(chalk_1.default.cyan("📋 설치 명령어:"));
312
+ console.log(chalk_1.default.yellow('\n⚠ 일부 도구가 설치되어 있지 않습니다.'));
313
+ console.log(chalk_1.default.gray(' SEMO의 일부 기능이 제한될 수 있습니다.\n'));
314
+ console.log(chalk_1.default.cyan('📋 설치 명령어:'));
310
315
  for (const tool of missingTools) {
311
316
  console.log(chalk_1.default.white(` ${tool.installCmd}`));
312
317
  if (tool.windowsAltCmds && tool.windowsAltCmds.length > 0) {
313
- console.log(chalk_1.default.gray(" (대체 방법)"));
318
+ console.log(chalk_1.default.gray(' (대체 방법)'));
314
319
  for (const altCmd of tool.windowsAltCmds) {
315
320
  console.log(chalk_1.default.gray(` ${altCmd}`));
316
321
  }
@@ -319,9 +324,9 @@ async function showToolsStatus() {
319
324
  console.log();
320
325
  const { continueWithout } = await inquirer_1.default.prompt([
321
326
  {
322
- type: "confirm",
323
- name: "continueWithout",
324
- message: "도구 없이 계속 설치를 진행할까요?",
327
+ type: 'confirm',
328
+ name: 'continueWithout',
329
+ message: '도구 없이 계속 설치를 진행할까요?',
325
330
  default: true,
326
331
  },
327
332
  ]);
@@ -332,52 +337,52 @@ async function showToolsStatus() {
332
337
  // === 글로벌 설정 체크 ===
333
338
  function isGlobalSetupDone() {
334
339
  const home = os.homedir();
335
- const hasEnv = fs.existsSync(path.join(home, ".claude", "semo", ".env"));
336
- const hasSetup = fs.existsSync(path.join(home, ".claude", "semo", "SOUL.md")) ||
337
- fs.existsSync(path.join(home, ".claude", "skills")); // 하위 호환
340
+ const hasEnv = fs.existsSync(path.join(home, '.claude', 'semo', '.env'));
341
+ const hasSetup = fs.existsSync(path.join(home, '.claude', 'semo', 'SOUL.md')) ||
342
+ fs.existsSync(path.join(home, '.claude', 'skills')); // 하위 호환
338
343
  return hasEnv && hasSetup;
339
344
  }
340
345
  // === onboarding 명령어 (글로벌 설정 — init 통합) ===
341
346
  program
342
- .command("onboarding")
343
- .description("글로벌 SEMO 설정 — ~/.claude/semo/, skills/agents/commands")
344
- .option("--credentials-gist <gistId>", "Private GitHub Gist에서 DB 접속정보 가져오기")
345
- .option("-f, --force", "기존 설정 덮어쓰기")
346
- .option("--skip-mcp", "MCP 설정 생략")
347
- .option("--skip-bots", "봇 워크스페이스 미러 건너뛰기")
347
+ .command('onboarding')
348
+ .description('글로벌 SEMO 설정 — ~/.claude/semo/, skills/agents/commands')
349
+ .option('--credentials-gist <gistId>', 'Private GitHub Gist에서 DB 접속정보 가져오기')
350
+ .option('-f, --force', '기존 설정 덮어쓰기')
351
+ .option('--skip-mcp', 'MCP 설정 생략')
352
+ .option('--skip-bots', '봇 워크스페이스 미러 건너뛰기')
348
353
  .action(async (options) => {
349
- console.log(chalk_1.default.cyan.bold("\n🏠 SEMO 온보딩\n"));
350
- console.log(chalk_1.default.gray(" 대상: ~/.claude/semo/ (머신당 1회)\n"));
354
+ console.log(chalk_1.default.cyan.bold('\n🏠 SEMO 온보딩\n'));
355
+ console.log(chalk_1.default.gray(' 대상: ~/.claude/semo/ (머신당 1회)\n'));
351
356
  // 1. ~/.claude/semo/.env DB 접속 설정
352
357
  await setupSemoEnv(options.credentialsGist, options.force);
353
358
  // 2. DB health check
354
- const spinner = (0, ora_1.default)("DB 연결 확인 중...").start();
359
+ const spinner = (0, ora_1.default)('DB 연결 확인 중...').start();
355
360
  const connected = await (0, database_1.isDbConnected)();
356
361
  if (connected) {
357
- spinner.succeed("DB 연결 확인됨");
362
+ spinner.succeed('DB 연결 확인됨');
358
363
  }
359
364
  else {
360
- spinner.warn("DB 연결 실패 — 스킬/봇 미러 설치를 건너뜁니다");
365
+ spinner.warn('DB 연결 실패 — 스킬/봇 미러 설치를 건너뜁니다');
361
366
  console.log(chalk_1.default.gray([
362
- "",
363
- " 흔한 원인:",
364
- " 1. SSH 터널 미실행 — 로컬에서는 SSH 터널이 필요합니다:",
365
- " ssh -J opc@152.70.244.169 -L 15432:localhost:5432 opc@10.0.0.91 -N -i ~/.ssh/oci_dev_rsa",
366
- " 2. ~/.claude/semo/.env의 DATABASE_URL 확인",
367
- "",
368
- " 터널 실행 후 다시 시도: semo onboarding",
369
- "",
370
- ].join("\n")));
367
+ '',
368
+ ' 흔한 원인:',
369
+ ' 1. SSH 터널 미실행 — 로컬에서는 SSH 터널이 필요합니다:',
370
+ ' ssh -J opc@152.70.244.169 -L 15432:localhost:5432 opc@10.0.0.91 -N -i ~/.ssh/oci_dev_rsa',
371
+ ' 2. ~/.claude/semo/.env의 DATABASE_URL 확인',
372
+ '',
373
+ ' 터널 실행 후 다시 시도: semo onboarding',
374
+ '',
375
+ ].join('\n')));
371
376
  await (0, database_1.closeConnection)();
372
377
  return;
373
378
  }
374
379
  // 3. ~/.claude/semo/ 디렉토리 구조 생성
375
- console.log(chalk_1.default.cyan("\n📂 SEMO 워크스페이스 구성 (~/.claude/semo/)"));
380
+ console.log(chalk_1.default.cyan('\n📂 SEMO 워크스페이스 구성 (~/.claude/semo/)'));
376
381
  (0, semo_workspace_1.ensureSemoDir)();
377
- console.log(chalk_1.default.green(" ✓ ~/.claude/semo/ 디렉토리 생성됨"));
382
+ console.log(chalk_1.default.green(' ✓ ~/.claude/semo/ 디렉토리 생성됨'));
378
383
  // 4. 봇 워크스페이스 미러 (DB → semo/bots/)
379
384
  if (!options.skipBots) {
380
- const mirrorSpinner = (0, ora_1.default)("봇 워크스페이스 미러링 (DB → semo/bots/)...").start();
385
+ const mirrorSpinner = (0, ora_1.default)('봇 워크스페이스 미러링 (DB → semo/bots/)...').start();
381
386
  try {
382
387
  const result = await (0, semo_workspace_1.populateBotMirrors)();
383
388
  mirrorSpinner.succeed(`봇 미러 완료: ${result.bots}개 봇, ${result.files}개 파일`);
@@ -387,20 +392,20 @@ program
387
392
  }
388
393
  }
389
394
  else {
390
- console.log(chalk_1.default.gray(" → 봇 미러 건너뜀 (--skip-bots)"));
395
+ console.log(chalk_1.default.gray(' → 봇 미러 건너뜀 (--skip-bots)'));
391
396
  }
392
397
  // 5. SOUL.md / MEMORY.md / USER.md 생성
393
398
  try {
394
399
  await (0, semo_workspace_1.generateSoulMd)();
395
- console.log(chalk_1.default.green(" ✓ semo/SOUL.md 생성됨 (오케스트레이터 페르소나)"));
400
+ console.log(chalk_1.default.green(' ✓ semo/SOUL.md 생성됨 (오케스트레이터 페르소나)'));
396
401
  }
397
402
  catch (err) {
398
403
  console.log(chalk_1.default.yellow(` ⚠ SOUL.md 생성 실패: ${err}`));
399
404
  }
400
405
  (0, semo_workspace_1.generateMemoryMd)();
401
- console.log(chalk_1.default.green(" ✓ semo/MEMORY.md 생성됨 (KB 인덱스)"));
406
+ console.log(chalk_1.default.green(' ✓ semo/MEMORY.md 생성됨 (KB 인덱스)'));
402
407
  (0, semo_workspace_1.generateUserMd)();
403
- console.log(chalk_1.default.green(" ✓ semo/USER.md 확인됨 (사용자 프로필)"));
408
+ console.log(chalk_1.default.green(' ✓ semo/USER.md 확인됨 (사용자 프로필)'));
404
409
  // 6. Standard 설치 (DB → ~/.claude/skills, commands, agents)
405
410
  await setupStandardGlobal();
406
411
  // 7. Hooks 설치
@@ -410,129 +415,129 @@ program
410
415
  await setupMCP(os.homedir(), [], options.force || false);
411
416
  }
412
417
  // 9. Thin Router CLAUDE.md 생성
413
- console.log(chalk_1.default.cyan("\n📄 CLAUDE.md 라우터 생성"));
418
+ console.log(chalk_1.default.cyan('\n📄 CLAUDE.md 라우터 생성'));
414
419
  const kbFirstBlock = await buildKbFirstBlock();
415
420
  (0, semo_workspace_1.generateThinRouter)(kbFirstBlock);
416
- console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md (thin router) 생성됨"));
421
+ console.log(chalk_1.default.green(' ✓ ~/.claude/CLAUDE.md (thin router) 생성됨'));
417
422
  await (0, database_1.closeConnection)();
418
423
  // 결과 요약
419
- console.log(chalk_1.default.green.bold("\n✅ SEMO 온보딩 완료!\n"));
420
- console.log(chalk_1.default.cyan("설치된 구성:"));
421
- console.log(chalk_1.default.gray(" ~/.claude/semo/.env DB 접속정보 (권한 600)"));
422
- console.log(chalk_1.default.gray(" ~/.claude/semo/SOUL.md 오케스트레이터 페르소나"));
423
- console.log(chalk_1.default.gray(" ~/.claude/semo/MEMORY.md KB 접근 가이드"));
424
- console.log(chalk_1.default.gray(" ~/.claude/semo/USER.md 사용자 프로필"));
425
- console.log(chalk_1.default.gray(" ~/.claude/semo/bots/ 봇 워크스페이스 미러"));
426
- console.log(chalk_1.default.gray(" ~/.claude/skills/ 팀 스킬 (DB 기반)"));
427
- console.log(chalk_1.default.gray(" ~/.claude/commands/ 팀 커맨드 (DB 기반)"));
428
- console.log(chalk_1.default.gray(" ~/.claude/agents/ 팀 에이전트 (DB 기반)"));
429
- console.log(chalk_1.default.gray(" ~/.claude/CLAUDE.md Thin router + KB-First"));
430
- console.log(chalk_1.default.gray(" ~/.claude/settings.local.json SessionStart/Stop 훅"));
431
- console.log(chalk_1.default.cyan("\n다음 단계:"));
432
- console.log(chalk_1.default.gray(" Claude Code에서 프로젝트를 열면 SessionStart 훅이 자동으로 sync합니다."));
424
+ console.log(chalk_1.default.green.bold('\n✅ SEMO 온보딩 완료!\n'));
425
+ console.log(chalk_1.default.cyan('설치된 구성:'));
426
+ console.log(chalk_1.default.gray(' ~/.claude/semo/.env DB 접속정보 (권한 600)'));
427
+ console.log(chalk_1.default.gray(' ~/.claude/semo/SOUL.md 오케스트레이터 페르소나'));
428
+ console.log(chalk_1.default.gray(' ~/.claude/semo/MEMORY.md KB 접근 가이드'));
429
+ console.log(chalk_1.default.gray(' ~/.claude/semo/USER.md 사용자 프로필'));
430
+ console.log(chalk_1.default.gray(' ~/.claude/semo/bots/ 봇 워크스페이스 미러'));
431
+ console.log(chalk_1.default.gray(' ~/.claude/skills/ 팀 스킬 (DB 기반)'));
432
+ console.log(chalk_1.default.gray(' ~/.claude/commands/ 팀 커맨드 (DB 기반)'));
433
+ console.log(chalk_1.default.gray(' ~/.claude/agents/ 팀 에이전트 (DB 기반)'));
434
+ console.log(chalk_1.default.gray(' ~/.claude/CLAUDE.md Thin router + KB-First'));
435
+ console.log(chalk_1.default.gray(' ~/.claude/settings.local.json SessionStart/Stop 훅'));
436
+ console.log(chalk_1.default.cyan('\n다음 단계:'));
437
+ console.log(chalk_1.default.gray(' Claude Code에서 프로젝트를 열면 SessionStart 훅이 자동으로 sync합니다.'));
433
438
  console.log();
434
439
  });
435
440
  // === init 명령어 (deprecated — onboarding으로 통합됨) ===
436
441
  program
437
- .command("init")
438
- .description("[deprecated] semo onboarding으로 통합되었습니다")
442
+ .command('init')
443
+ .description('[deprecated] semo onboarding으로 통합되었습니다')
439
444
  .action(async () => {
440
445
  console.log(chalk_1.default.yellow("\n⚠ 'semo init'은 'semo onboarding'으로 통합되었습니다.\n"));
441
- console.log(chalk_1.default.cyan(" 글로벌 설정이 필요하면:"));
442
- console.log(chalk_1.default.gray(" semo onboarding\n"));
443
- console.log(chalk_1.default.cyan(" 이미 온보딩을 완료했다면:"));
444
- console.log(chalk_1.default.gray(" Claude Code에서 프로젝트를 열면 SessionStart 훅이 자동으로 sync합니다.\n"));
446
+ console.log(chalk_1.default.cyan(' 글로벌 설정이 필요하면:'));
447
+ console.log(chalk_1.default.gray(' semo onboarding\n'));
448
+ console.log(chalk_1.default.cyan(' 이미 온보딩을 완료했다면:'));
449
+ console.log(chalk_1.default.gray(' Claude Code에서 프로젝트를 열면 SessionStart 훅이 자동으로 sync합니다.\n'));
445
450
  });
446
451
  // === Standard 설치 (DB 기반, 글로벌 ~/.claude/) ===
447
452
  async function setupStandardGlobal() {
448
- console.log(chalk_1.default.cyan("\n📚 Standard 설치 (DB → ~/.claude/)"));
449
- console.log(chalk_1.default.gray(" 스킬/커맨드/에이전트를 글로벌에 설치\n"));
450
- const spinner = (0, ora_1.default)("DB에서 스킬/커맨드/에이전트 조회 중...").start();
453
+ console.log(chalk_1.default.cyan('\n📚 Standard 설치 (DB → ~/.claude/)'));
454
+ console.log(chalk_1.default.gray(' 스킬/커맨드/에이전트를 글로벌에 설치\n'));
455
+ const spinner = (0, ora_1.default)('DB에서 스킬/커맨드/에이전트 조회 중...').start();
451
456
  try {
452
457
  const connected = await (0, database_1.isDbConnected)();
453
458
  if (connected) {
454
- spinner.text = "DB 연결 성공, 데이터 조회 중...";
459
+ spinner.text = 'DB 연결 성공, 데이터 조회 중...';
455
460
  }
456
461
  else {
457
- spinner.text = "DB 연결 실패, 폴백 데이터 사용 중...";
462
+ spinner.text = 'DB 연결 실패, 폴백 데이터 사용 중...';
458
463
  }
459
464
  const result = await (0, global_cache_1.syncGlobalCache)();
460
465
  console.log(chalk_1.default.green(` ✓ skills 설치 완료 (${result.skills}개)`));
461
466
  console.log(chalk_1.default.green(` ✓ commands 설치 완료 (${result.commands}개)`));
462
467
  console.log(chalk_1.default.green(` ✓ agents 설치 완료 (${result.agents}개)`));
463
- spinner.succeed("Standard 설치 완료 (DB → ~/.claude/)");
468
+ spinner.succeed('Standard 설치 완료 (DB → ~/.claude/)');
464
469
  }
465
470
  catch (error) {
466
- spinner.fail("Standard 설치 실패");
471
+ spinner.fail('Standard 설치 실패');
467
472
  console.error(chalk_1.default.red(` ${error}`));
468
473
  }
469
474
  }
470
475
  const BASE_MCP_SERVERS = [
471
476
  {
472
- name: "context7",
473
- command: "npx",
474
- args: ["-y", "@upstash/context7-mcp"],
475
- scope: "user",
477
+ name: 'context7',
478
+ command: 'npx',
479
+ args: ['-y', '@upstash/context7-mcp'],
480
+ scope: 'user',
476
481
  },
477
482
  {
478
- name: "sequential-thinking",
479
- command: "npx",
480
- args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
481
- scope: "user",
483
+ name: 'sequential-thinking',
484
+ command: 'npx',
485
+ args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
486
+ scope: 'user',
482
487
  },
483
488
  {
484
- name: "playwright",
485
- command: "npx",
486
- args: ["-y", "@anthropic-ai/mcp-server-playwright"],
487
- scope: "user",
489
+ name: 'playwright',
490
+ command: 'npx',
491
+ args: ['-y', '@anthropic-ai/mcp-server-playwright'],
492
+ scope: 'user',
488
493
  },
489
494
  {
490
- name: "github",
491
- command: "npx",
492
- args: ["-y", "@modelcontextprotocol/server-github"],
493
- scope: "user",
495
+ name: 'github',
496
+ command: 'npx',
497
+ args: ['-y', '@modelcontextprotocol/server-github'],
498
+ scope: 'user',
494
499
  },
495
500
  ];
496
501
  // === ~/.claude/semo/.env 설정 (자동 감지 → Gist → 프롬프트) ===
497
- const SEMO_ENV_PATH = path.join(os.homedir(), ".claude", "semo", ".env");
502
+ const SEMO_ENV_PATH = path.join(os.homedir(), '.claude', 'semo', '.env');
498
503
  const SEMO_CREDENTIALS = [
499
504
  {
500
- key: "DATABASE_URL",
505
+ key: 'DATABASE_URL',
501
506
  required: true,
502
507
  sensitive: true,
503
- description: "팀 코어 PostgreSQL 연결 URL",
504
- promptMessage: "DATABASE_URL:",
508
+ description: '팀 코어 PostgreSQL 연결 URL',
509
+ promptMessage: 'DATABASE_URL:',
505
510
  },
506
511
  {
507
- key: "OPENAI_API_KEY",
512
+ key: 'OPENAI_API_KEY',
508
513
  required: false,
509
514
  sensitive: true,
510
- description: "OpenAI API 키 (KB 임베딩용)",
511
- promptMessage: "OPENAI_API_KEY (없으면 Enter):",
515
+ description: 'OpenAI API 키 (KB 임베딩용)',
516
+ promptMessage: 'OPENAI_API_KEY (없으면 Enter):',
512
517
  },
513
518
  {
514
- key: "SLACK_WEBHOOK",
519
+ key: 'SLACK_WEBHOOK',
515
520
  required: false,
516
521
  sensitive: false,
517
- description: "Slack 알림 Webhook (선택)",
518
- promptMessage: "SLACK_WEBHOOK (없으면 Enter):",
522
+ description: 'Slack 알림 Webhook (선택)',
523
+ promptMessage: 'SLACK_WEBHOOK (없으면 Enter):',
519
524
  },
520
525
  ];
521
526
  function writeSemoEnvFile(creds) {
522
527
  // 디렉토리 보장
523
528
  fs.mkdirSync(path.dirname(SEMO_ENV_PATH), { recursive: true });
524
529
  const lines = [
525
- "# SEMO 환경변수 — 모든 컨텍스트에서 자동 로드됨",
526
- "# (Claude Code 앱, OpenClaw LaunchAgent, cron 등)",
527
- "# 경로: ~/.claude/semo/.env (v4.5.0+)",
528
- "",
530
+ '# SEMO 환경변수 — 모든 컨텍스트에서 자동 로드됨',
531
+ '# (Claude Code 앱, OpenClaw LaunchAgent, cron 등)',
532
+ '# 경로: ~/.claude/semo/.env (v4.5.0+)',
533
+ '',
529
534
  ];
530
535
  // 레지스트리 키 먼저 (순서 보장)
531
536
  for (const def of SEMO_CREDENTIALS) {
532
- const val = creds[def.key] || "";
537
+ const val = creds[def.key] || '';
533
538
  lines.push(`# ${def.description}`);
534
539
  lines.push(`${def.key}='${val}'`);
535
- lines.push("");
540
+ lines.push('');
536
541
  }
537
542
  // 레지스트리 외 추가 키
538
543
  for (const [k, v] of Object.entries(creds)) {
@@ -540,15 +545,15 @@ function writeSemoEnvFile(creds) {
540
545
  lines.push(`${k}='${v}'`);
541
546
  }
542
547
  }
543
- lines.push("");
544
- fs.writeFileSync(SEMO_ENV_PATH, lines.join("\n"), { mode: 0o600 });
548
+ lines.push('');
549
+ fs.writeFileSync(SEMO_ENV_PATH, lines.join('\n'), { mode: 0o600 });
545
550
  }
546
551
  function readSemoEnvCreds() {
547
552
  const envFile = SEMO_ENV_PATH;
548
553
  if (!fs.existsSync(envFile))
549
554
  return {};
550
555
  try {
551
- return (0, env_parser_1.parseEnvContent)(fs.readFileSync(envFile, "utf-8"));
556
+ return (0, env_parser_1.parseEnvContent)(fs.readFileSync(envFile, 'utf-8'));
552
557
  }
553
558
  catch {
554
559
  return {};
@@ -557,8 +562,8 @@ function readSemoEnvCreds() {
557
562
  function fetchCredsFromGist(gistId) {
558
563
  try {
559
564
  const raw = (0, child_process_1.execSync)(`gh gist view ${gistId} --raw`, {
560
- encoding: "utf-8",
561
- stdio: ["pipe", "pipe", "pipe"],
565
+ encoding: 'utf-8',
566
+ stdio: ['pipe', 'pipe', 'pipe'],
562
567
  timeout: 8000,
563
568
  });
564
569
  const creds = (0, env_parser_1.parseEnvContent)(raw);
@@ -569,14 +574,14 @@ function fetchCredsFromGist(gistId) {
569
574
  }
570
575
  }
571
576
  async function setupSemoEnv(credentialsGist, force) {
572
- console.log(chalk_1.default.cyan("\n🔑 환경변수 설정"));
577
+ console.log(chalk_1.default.cyan('\n🔑 환경변수 설정'));
573
578
  // 1. 기존 파일
574
579
  const existing = force ? {} : readSemoEnvCreds();
575
580
  // 2. Gist
576
581
  const gistId = credentialsGist || process.env.SEMO_CREDENTIALS_GIST;
577
582
  let gistCreds = {};
578
583
  if (gistId) {
579
- console.log(chalk_1.default.gray(" GitHub Gist에서 팀 접속정보 가져오는 중..."));
584
+ console.log(chalk_1.default.gray(' GitHub Gist에서 팀 접속정보 가져오는 중...'));
580
585
  gistCreds = fetchCredsFromGist(gistId) || {};
581
586
  }
582
587
  // 3. 머지: gist(base) ← file(override) ← env(override)
@@ -593,16 +598,16 @@ async function setupSemoEnv(credentialsGist, force) {
593
598
  let hasNewKeys = false;
594
599
  for (const def of SEMO_CREDENTIALS) {
595
600
  if (merged[def.key]) {
596
- const label = def.sensitive ? "(설정됨)" : merged[def.key];
601
+ const label = def.sensitive ? '(설정됨)' : merged[def.key];
597
602
  console.log(chalk_1.default.green(` ✅ ${def.key} ${label}`));
598
603
  }
599
604
  else if (def.required) {
600
605
  const { value } = await inquirer_1.default.prompt([
601
606
  {
602
- type: "password",
603
- name: "value",
607
+ type: 'password',
608
+ name: 'value',
604
609
  message: def.promptMessage,
605
- mask: "*",
610
+ mask: '*',
606
611
  },
607
612
  ]);
608
613
  if (value?.trim()) {
@@ -621,16 +626,16 @@ async function setupSemoEnv(credentialsGist, force) {
621
626
  Object.keys(gistCreds).some((k) => !existing[k]);
622
627
  if (needsWrite) {
623
628
  writeSemoEnvFile(merged);
624
- console.log(chalk_1.default.green(" ✅ ~/.claude/semo/.env 저장됨 (권한: 600)"));
629
+ console.log(chalk_1.default.green(' ✅ ~/.claude/semo/.env 저장됨 (권한: 600)'));
625
630
  }
626
631
  else {
627
- console.log(chalk_1.default.gray(" ~/.claude/semo/.env 변경 없음"));
632
+ console.log(chalk_1.default.gray(' ~/.claude/semo/.env 변경 없음'));
628
633
  }
629
634
  }
630
635
  // === Claude MCP 서버 존재 여부 확인 ===
631
636
  function isMCPServerRegistered(serverName) {
632
637
  try {
633
- const result = (0, child_process_1.execSync)("claude mcp list", { stdio: "pipe", encoding: "utf-8" });
638
+ const result = (0, child_process_1.execSync)('claude mcp list', { stdio: 'pipe', encoding: 'utf-8' });
634
639
  return result.includes(serverName);
635
640
  }
636
641
  catch {
@@ -646,19 +651,19 @@ function registerMCPServer(server) {
646
651
  }
647
652
  // claude mcp add 명령어 구성
648
653
  // 형식: claude mcp add <name> [-e KEY=value...] -- <command> [args...]
649
- const args = ["mcp", "add", server.name];
654
+ const args = ['mcp', 'add', server.name];
650
655
  // 환경변수가 있는 경우 -e 옵션 추가
651
656
  if (server.env) {
652
657
  for (const [key, value] of Object.entries(server.env)) {
653
- args.push("-e", `${key}=${value}`);
658
+ args.push('-e', `${key}=${value}`);
654
659
  }
655
660
  }
656
661
  // scope 지정 (기본: project)
657
- const scope = server.scope || "project";
658
- args.push("-s", scope);
662
+ const scope = server.scope || 'project';
663
+ args.push('-s', scope);
659
664
  // -- 구분자 후 명령어와 인자 추가
660
- args.push("--", server.command, ...server.args);
661
- (0, child_process_1.execSync)(`claude ${args.join(" ")}`, { stdio: "pipe" });
665
+ args.push('--', server.command, ...server.args);
666
+ (0, child_process_1.execSync)(`claude ${args.join(' ')}`, { stdio: 'pipe' });
662
667
  return { success: true };
663
668
  }
664
669
  catch (error) {
@@ -666,10 +671,10 @@ function registerMCPServer(server) {
666
671
  }
667
672
  }
668
673
  // === 글로벌 CLAUDE.md에 KB-First 규칙 주입 ===
669
- const KB_FIRST_SECTION_MARKER = "## SEMO KB-First 행동 규칙";
674
+ const KB_FIRST_SECTION_MARKER = '## SEMO KB-First 행동 규칙';
670
675
  async function buildKbFirstBlock() {
671
676
  // DB에서 온톨로지 + 타입스키마를 조회해서 동적 생성
672
- let domainGuide = "";
677
+ let domainGuide = '';
673
678
  try {
674
679
  const pool = (0, database_1.getPool)();
675
680
  // 1. 타입스키마: 타입별 scheme_key 목록
@@ -678,7 +683,11 @@ async function buildKbFirstBlock() {
678
683
  const typeSchemas = new Map();
679
684
  for (const r of schemaRows.rows) {
680
685
  const entries = typeSchemas.get(r.type_key) || [];
681
- entries.push({ scheme_key: r.scheme_key, required: r.required, desc: r.scheme_description || "" });
686
+ entries.push({
687
+ scheme_key: r.scheme_key,
688
+ required: r.required,
689
+ desc: r.scheme_description || '',
690
+ });
682
691
  typeSchemas.set(r.type_key, entries);
683
692
  }
684
693
  // 2. 온톨로지: 엔티티 타입별 도메인 목록
@@ -686,37 +695,47 @@ async function buildKbFirstBlock() {
686
695
  const entities = new Map();
687
696
  for (const r of ontoRows.rows) {
688
697
  const list = entities.get(r.entity_type) || [];
689
- list.push({ domain: r.domain, desc: r.description || "" });
698
+ list.push({ domain: r.domain, desc: r.description || '' });
690
699
  entities.set(r.entity_type, list);
691
700
  }
692
701
  // 3. 도메인 가이드 생성
693
- const orgDomains = entities.get("organization") || [];
694
- const svcDomains = entities.get("service") || [];
695
- const orgSchema = typeSchemas.get("organization") || [];
696
- const svcSchema = typeSchemas.get("service") || [];
697
- domainGuide += "#### 도메인 구조\n";
698
- domainGuide += "| 패턴 | 예시 | 용도 |\n|------|------|------|\n";
702
+ const orgDomains = entities.get('organization') || [];
703
+ const svcDomains = entities.get('service') || [];
704
+ const orgSchema = typeSchemas.get('organization') || [];
705
+ const svcSchema = typeSchemas.get('service') || [];
706
+ domainGuide += '#### 도메인 구조\n';
707
+ domainGuide += '| 패턴 | 예시 | 용도 |\n|------|------|------|\n';
699
708
  if (orgDomains.length > 0) {
700
- const orgEx = orgDomains.map(o => o.domain).join(", ");
701
- const orgKeys = orgSchema.filter(s => !s.scheme_key.includes("{")).map(s => s.scheme_key).join(", ");
709
+ const orgEx = orgDomains.map((o) => o.domain).join(', ');
710
+ const orgKeys = orgSchema
711
+ .filter((s) => !s.scheme_key.includes('{'))
712
+ .map((s) => s.scheme_key)
713
+ .join(', ');
702
714
  domainGuide += `| 조직 도메인 | \`${orgEx}\` | ${orgKeys} 등 조직 정보 |\n`;
703
715
  }
704
716
  if (svcDomains.length > 0) {
705
- const svcEx = svcDomains.slice(0, 5).map(s => s.domain).join(", ");
706
- const svcKeys = svcSchema.filter(s => !s.scheme_key.includes("{")).map(s => s.scheme_key).join(", ");
717
+ const svcEx = svcDomains
718
+ .slice(0, 5)
719
+ .map((s) => s.domain)
720
+ .join(', ');
721
+ const svcKeys = svcSchema
722
+ .filter((s) => !s.scheme_key.includes('{'))
723
+ .map((s) => s.scheme_key)
724
+ .join(', ');
707
725
  domainGuide += `| 서비스 도메인 | \`${svcEx}\` 등 ${svcDomains.length}개 | ${svcKeys} 등 서비스 정보 |\n`;
708
726
  }
709
- domainGuide += "\n#### 읽기 예시 (Query-First)\n";
710
- domainGuide += "다음 주제 질문 → **반드시 `semo kb search`/`semo kb get`으로 KB 먼저 조회** 후 답변:\n";
727
+ domainGuide += '\n#### 읽기 예시 (Query-First)\n';
728
+ domainGuide +=
729
+ '다음 주제 질문 → **반드시 `semo kb search`/`semo kb get`으로 KB 먼저 조회** 후 답변:\n';
711
730
  // 조직 도메인 키 가이드
712
731
  for (const s of orgSchema) {
713
- if (s.scheme_key.includes("{")) {
732
+ if (s.scheme_key.includes('{')) {
714
733
  const label = s.desc || s.scheme_key;
715
- domainGuide += `- ${label} → \`domain: ${orgDomains[0]?.domain || "semicolon"}\`, key: \`${s.scheme_key}\`\n`;
734
+ domainGuide += `- ${label} → \`domain: ${orgDomains[0]?.domain || 'semicolon'}\`, key: \`${s.scheme_key}\`\n`;
716
735
  }
717
736
  }
718
737
  // 서비스 도메인 키 가이드
719
- for (const s of svcSchema.filter(s => s.required && !s.scheme_key.includes("{"))) {
738
+ for (const s of svcSchema.filter((s) => s.required && !s.scheme_key.includes('{'))) {
720
739
  const label = s.desc || s.scheme_key;
721
740
  domainGuide += `- 서비스 ${label} → \`domain: {서비스명}\`, key: \`${s.scheme_key}\`\n`;
722
741
  }
@@ -754,13 +773,13 @@ KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하
754
773
  // injectKbFirstToGlobalClaudeMd 제거 — generateThinRouter()로 대체 (semo-workspace.ts)
755
774
  // === MCP 설정 ===
756
775
  async function setupMCP(cwd, _extensions, force) {
757
- console.log(chalk_1.default.cyan("\n🔧 Black Box 설정 (MCP Server)"));
758
- console.log(chalk_1.default.gray(" 토큰이 격리된 외부 연동 도구\n"));
759
- const settingsPath = path.join(cwd, ".claude", "settings.json");
776
+ console.log(chalk_1.default.cyan('\n🔧 Black Box 설정 (MCP Server)'));
777
+ console.log(chalk_1.default.gray(' 토큰이 격리된 외부 연동 도구\n'));
778
+ const settingsPath = path.join(cwd, '.claude', 'settings.json');
760
779
  if (fs.existsSync(settingsPath) && !force) {
761
- const shouldOverwrite = await confirmOverwrite(".claude/settings.json", settingsPath);
780
+ const shouldOverwrite = await confirmOverwrite('.claude/settings.json', settingsPath);
762
781
  if (!shouldOverwrite) {
763
- console.log(chalk_1.default.gray(" → settings.json 건너뜀"));
782
+ console.log(chalk_1.default.gray(' → settings.json 건너뜀'));
764
783
  return;
765
784
  }
766
785
  }
@@ -770,9 +789,9 @@ async function setupMCP(cwd, _extensions, force) {
770
789
  };
771
790
  // 공통 서버(context7 등)는 유저레벨에 등록하므로 프로젝트 settings에 쓰지 않음
772
791
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
773
- console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨"));
792
+ console.log(chalk_1.default.green('✓ .claude/settings.json 생성됨'));
774
793
  // Claude Code에 MCP 서버 등록 시도 (공통 서버는 유저레벨로)
775
- console.log(chalk_1.default.cyan("\n🔌 Claude Code에 MCP 서버 등록 중..."));
794
+ console.log(chalk_1.default.cyan('\n🔌 Claude Code에 MCP 서버 등록 중...'));
776
795
  const allServers = [...BASE_MCP_SERVERS];
777
796
  const successServers = [];
778
797
  const skippedServers = [];
@@ -805,13 +824,15 @@ async function setupMCP(cwd, _extensions, force) {
805
824
  // 실패한 서버가 있으면 수동 등록 안내
806
825
  if (failedServers.length > 0) {
807
826
  console.log(chalk_1.default.yellow(`\n⚠ ${failedServers.length}개 MCP 서버 자동 등록 실패`));
808
- console.log(chalk_1.default.cyan("\n📋 수동 등록 명령어:"));
809
- console.log(chalk_1.default.gray(" 다음 명령어를 터미널에서 실행하세요:\n"));
827
+ console.log(chalk_1.default.cyan('\n📋 수동 등록 명령어:'));
828
+ console.log(chalk_1.default.gray(' 다음 명령어를 터미널에서 실행하세요:\n'));
810
829
  for (const server of failedServers) {
811
830
  const envArgs = server.env
812
- ? Object.entries(server.env).map(([k, v]) => `-e ${k}="${v}"`).join(" ")
813
- : "";
814
- const cmd = `claude mcp add ${server.name} ${envArgs} -- ${server.command} ${server.args.join(" ")}`.trim();
831
+ ? Object.entries(server.env)
832
+ .map(([k, v]) => `-e ${k}="${v}"`)
833
+ .join(' ')
834
+ : '';
835
+ const cmd = `claude mcp add ${server.name} ${envArgs} -- ${server.command} ${server.args.join(' ')}`.trim();
815
836
  console.log(chalk_1.default.white(` ${cmd}`));
816
837
  }
817
838
  console.log();
@@ -820,47 +841,65 @@ async function setupMCP(cwd, _extensions, force) {
820
841
  // updateGitignore 제거 — init 통합으로 프로젝트별 .gitignore 수정 불필요
821
842
  // === Hooks 설치/업데이트 ===
822
843
  async function setupHooks(isUpdate = false) {
823
- const action = isUpdate ? "업데이트" : "설치";
844
+ const action = isUpdate ? '업데이트' : '설치';
824
845
  console.log(chalk_1.default.cyan(`\n🪝 Claude Code Hooks ${action}`));
825
- console.log(chalk_1.default.gray(" semo CLI 기반 컨텍스트 동기화\n"));
846
+ console.log(chalk_1.default.gray(' semo CLI 기반 컨텍스트 동기화\n'));
826
847
  const homeDir = os.homedir();
827
- const settingsPath = path.join(homeDir, ".claude", "settings.local.json");
828
- // hooks 설정 객체 — semo CLI 사용 (프로젝트 경로 무관)
848
+ const settingsPath = path.join(homeDir, '.claude', 'settings.local.json');
849
+ // hooks 설정 객체 — semo CLI + enforcement hooks (프로젝트 경로 무관)
850
+ const sharedHooksDir = path.join(homeDir, '.openclaw-shared', 'hooks');
829
851
  const hooksConfig = {
830
852
  SessionStart: [
831
853
  {
832
- matcher: "",
854
+ matcher: '',
833
855
  hooks: [
834
856
  {
835
- type: "command",
836
- command: ". ~/.claude/semo/.env 2>/dev/null; semo context sync 2>/dev/null || true",
857
+ type: 'command',
858
+ command: '. ~/.claude/semo/.env 2>/dev/null; semo context sync 2>/dev/null || true',
837
859
  timeout: 30,
838
860
  },
839
861
  ],
840
862
  },
841
863
  ],
864
+ UserPromptSubmit: [
865
+ {
866
+ matcher: '',
867
+ hooks: [
868
+ {
869
+ type: 'command',
870
+ command: `bash ${path.join(sharedHooksDir, 'context-router.sh')}`,
871
+ timeout: 3000,
872
+ },
873
+ ],
874
+ },
875
+ ],
842
876
  Stop: [
843
877
  {
844
- matcher: "",
878
+ matcher: '',
845
879
  hooks: [
846
880
  {
847
- type: "command",
848
- command: ". ~/.claude/semo/.env 2>/dev/null; semo context push 2>/dev/null || true",
881
+ type: 'command',
882
+ command: '. ~/.claude/semo/.env 2>/dev/null; semo context push 2>/dev/null || true',
849
883
  timeout: 30,
850
884
  },
885
+ {
886
+ type: 'command',
887
+ command: `bash ${path.join(sharedHooksDir, 'decision-reminder.sh')}`,
888
+ timeout: 15000,
889
+ },
851
890
  ],
852
891
  },
853
892
  ],
854
893
  };
855
894
  // 기존 설정 로드 또는 새로 생성
856
895
  let existingSettings = {};
857
- const claudeConfigDir = path.join(homeDir, ".claude");
896
+ const claudeConfigDir = path.join(homeDir, '.claude');
858
897
  if (!fs.existsSync(claudeConfigDir)) {
859
898
  fs.mkdirSync(claudeConfigDir, { recursive: true });
860
899
  }
861
900
  if (fs.existsSync(settingsPath)) {
862
901
  try {
863
- existingSettings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
902
+ existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
864
903
  }
865
904
  catch {
866
905
  existingSettings = {};
@@ -875,12 +914,12 @@ async function setupHooks(isUpdate = false) {
875
914
  }
876
915
  // === Context Mesh 초기화 ===
877
916
  async function setupContextMesh(cwd) {
878
- console.log(chalk_1.default.cyan("\n🧠 Context Mesh 초기화"));
879
- console.log(chalk_1.default.gray(" 세션 간 컨텍스트 영속화\n"));
880
- const memoryDir = path.join(cwd, ".claude", "memory");
917
+ console.log(chalk_1.default.cyan('\n🧠 Context Mesh 초기화'));
918
+ console.log(chalk_1.default.gray(' 세션 간 컨텍스트 영속화\n'));
919
+ const memoryDir = path.join(cwd, '.claude', 'memory');
881
920
  fs.mkdirSync(memoryDir, { recursive: true });
882
921
  // context.md
883
- const contextPath = path.join(memoryDir, "context.md");
922
+ const contextPath = path.join(memoryDir, 'context.md');
884
923
  if (!fs.existsSync(contextPath)) {
885
924
  const contextContent = `# Project Context
886
925
 
@@ -895,7 +934,7 @@ async function setupContextMesh(cwd) {
895
934
  |------|-----|
896
935
  | **이름** | ${path.basename(cwd)} |
897
936
  | **SEMO 버전** | ${VERSION} |
898
- | **설치일** | ${new Date().toISOString().split("T")[0]} |
937
+ | **설치일** | ${new Date().toISOString().split('T')[0]} |
899
938
 
900
939
  ---
901
940
 
@@ -911,13 +950,13 @@ _프로젝트 분석 후 자동으로 채워집니다._
911
950
 
912
951
  ---
913
952
 
914
- *마지막 업데이트: ${new Date().toISOString().split("T")[0]}*
953
+ *마지막 업데이트: ${new Date().toISOString().split('T')[0]}*
915
954
  `;
916
955
  fs.writeFileSync(contextPath, contextContent);
917
- console.log(chalk_1.default.green("✓ .claude/memory/context.md 생성됨"));
956
+ console.log(chalk_1.default.green('✓ .claude/memory/context.md 생성됨'));
918
957
  }
919
958
  // decisions.md
920
- const decisionsPath = path.join(memoryDir, "decisions.md");
959
+ const decisionsPath = path.join(memoryDir, 'decisions.md');
921
960
  if (!fs.existsSync(decisionsPath)) {
922
961
  const decisionsContent = `# Architecture Decisions
923
962
 
@@ -951,10 +990,10 @@ _아직 기록된 결정이 없습니다._
951
990
  \`\`\`
952
991
  `;
953
992
  fs.writeFileSync(decisionsPath, decisionsContent);
954
- console.log(chalk_1.default.green("✓ .claude/memory/decisions.md 생성됨"));
993
+ console.log(chalk_1.default.green('✓ .claude/memory/decisions.md 생성됨'));
955
994
  }
956
995
  // projects.md
957
- const projectsPath = path.join(memoryDir, "projects.md");
996
+ const projectsPath = path.join(memoryDir, 'projects.md');
958
997
  if (!fs.existsSync(projectsPath)) {
959
998
  const projectsContent = `# 프로젝트 별칭 매핑
960
999
 
@@ -1011,15 +1050,15 @@ _아직 기록된 결정이 없습니다._
1011
1050
 
1012
1051
  ---
1013
1052
 
1014
- *마지막 업데이트: ${new Date().toISOString().split("T")[0]}*
1053
+ *마지막 업데이트: ${new Date().toISOString().split('T')[0]}*
1015
1054
  `;
1016
1055
  fs.writeFileSync(projectsPath, projectsContent);
1017
- console.log(chalk_1.default.green("✓ .claude/memory/projects.md 생성됨"));
1056
+ console.log(chalk_1.default.green('✓ .claude/memory/projects.md 생성됨'));
1018
1057
  }
1019
1058
  // rules 디렉토리
1020
- const rulesDir = path.join(memoryDir, "rules");
1059
+ const rulesDir = path.join(memoryDir, 'rules');
1021
1060
  fs.mkdirSync(rulesDir, { recursive: true });
1022
- const rulesPath = path.join(rulesDir, "project-specific.md");
1061
+ const rulesPath = path.join(rulesDir, 'project-specific.md');
1023
1062
  if (!fs.existsSync(rulesPath)) {
1024
1063
  const rulesContent = `# Project-Specific Rules
1025
1064
 
@@ -1038,17 +1077,17 @@ _프로젝트별 코딩 규칙을 여기에 추가하세요._
1038
1077
  _SEMO 기본 규칙의 예외 사항을 여기에 추가하세요._
1039
1078
  `;
1040
1079
  fs.writeFileSync(rulesPath, rulesContent);
1041
- console.log(chalk_1.default.green("✓ .claude/memory/rules/project-specific.md 생성됨"));
1080
+ console.log(chalk_1.default.green('✓ .claude/memory/rules/project-specific.md 생성됨'));
1042
1081
  }
1043
1082
  }
1044
1083
  // === CLAUDE.md 생성 ===
1045
1084
  async function setupClaudeMd(cwd, _extensions, force) {
1046
- console.log(chalk_1.default.cyan("\n📄 CLAUDE.md 설정"));
1047
- const claudeMdPath = path.join(cwd, ".claude", "CLAUDE.md");
1085
+ console.log(chalk_1.default.cyan('\n📄 CLAUDE.md 설정'));
1086
+ const claudeMdPath = path.join(cwd, '.claude', 'CLAUDE.md');
1048
1087
  if (fs.existsSync(claudeMdPath) && !force) {
1049
- const shouldOverwrite = await confirmOverwrite("CLAUDE.md", claudeMdPath);
1088
+ const shouldOverwrite = await confirmOverwrite('CLAUDE.md', claudeMdPath);
1050
1089
  if (!shouldOverwrite) {
1051
- console.log(chalk_1.default.gray(" → CLAUDE.md 건너뜀"));
1090
+ console.log(chalk_1.default.gray(' → CLAUDE.md 건너뜀'));
1052
1091
  return;
1053
1092
  }
1054
1093
  }
@@ -1056,8 +1095,8 @@ async function setupClaudeMd(cwd, _extensions, force) {
1056
1095
  const kbFirstFull = await buildKbFirstBlock();
1057
1096
  // 프로젝트용: "## SEMO KB-First 행동 규칙" → "## KB-First 행동 규칙 (NON-NEGOTIABLE)"
1058
1097
  const kbFirstSection = kbFirstFull
1059
- .replace(KB_FIRST_SECTION_MARKER, "## KB-First 행동 규칙 (NON-NEGOTIABLE)")
1060
- .replace(/> semo CLI의 kb-manager 스킬을 통해.*\n/, "> KB는 팀의 Single Source of Truth이다. 아래 규칙은 예외 없이 적용된다.\n")
1098
+ .replace(KB_FIRST_SECTION_MARKER, '## KB-First 행동 규칙 (NON-NEGOTIABLE)')
1099
+ .replace(/> semo CLI의 kb-manager 스킬을 통해.*\n/, '> KB는 팀의 Single Source of Truth이다. 아래 규칙은 예외 없이 적용된다.\n')
1061
1100
  .trim();
1062
1101
  // 프로젝트 규칙만 (스킬/에이전트 목록은 글로벌 ~/.claude/에 있음)
1063
1102
  const claudeMdContent = `# SEMO Project Configuration
@@ -1117,69 +1156,69 @@ SessionStart 훅과 OpenClaw 게이트웨이 래퍼에서 자동 source됩니다
1117
1156
  > Generated by SEMO CLI v${VERSION}
1118
1157
  `;
1119
1158
  fs.writeFileSync(claudeMdPath, claudeMdContent);
1120
- console.log(chalk_1.default.green("✓ .claude/CLAUDE.md 생성됨"));
1159
+ console.log(chalk_1.default.green('✓ .claude/CLAUDE.md 생성됨'));
1121
1160
  }
1122
1161
  // === list 명령어 ===
1123
1162
  program
1124
- .command("list")
1125
- .description("설치된 SEMO 패키지 상태를 표시합니다")
1163
+ .command('list')
1164
+ .description('설치된 SEMO 패키지 상태를 표시합니다')
1126
1165
  .action(async () => {
1127
- console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록\n"));
1166
+ console.log(chalk_1.default.cyan.bold('\n📦 SEMO 패키지 목록\n'));
1128
1167
  // DB 패키지 목록
1129
1168
  try {
1130
1169
  const packages = await (0, database_1.getPackages)();
1131
1170
  if (packages.length > 0) {
1132
- console.log(chalk_1.default.white.bold("DB 패키지"));
1171
+ console.log(chalk_1.default.white.bold('DB 패키지'));
1133
1172
  for (const pkg of packages) {
1134
- console.log(` ${chalk_1.default.cyan(pkg.name)} - ${pkg.description || ""}`);
1173
+ console.log(` ${chalk_1.default.cyan(pkg.name)} - ${pkg.description || ''}`);
1135
1174
  }
1136
1175
  console.log();
1137
1176
  }
1138
1177
  else {
1139
- console.log(chalk_1.default.gray(" 등록된 패키지가 없습니다.\n"));
1178
+ console.log(chalk_1.default.gray(' 등록된 패키지가 없습니다.\n'));
1140
1179
  }
1141
1180
  }
1142
1181
  catch {
1143
- console.log(chalk_1.default.yellow(" DB 연결 실패 — ~/.claude/semo/.env를 확인하세요.\n"));
1182
+ console.log(chalk_1.default.yellow(' DB 연결 실패 — ~/.claude/semo/.env를 확인하세요.\n'));
1144
1183
  }
1145
1184
  });
1146
1185
  // === status 명령어 ===
1147
1186
  program
1148
- .command("status")
1149
- .description("SEMO 설치 상태를 확인합니다")
1187
+ .command('status')
1188
+ .description('SEMO 설치 상태를 확인합니다')
1150
1189
  .action(async () => {
1151
- console.log(chalk_1.default.cyan.bold("\n📊 SEMO 설치 상태\n"));
1190
+ console.log(chalk_1.default.cyan.bold('\n📊 SEMO 설치 상태\n'));
1152
1191
  const home = os.homedir();
1153
1192
  // 글로벌 설정 확인
1154
- console.log(chalk_1.default.white.bold("글로벌 설정 (~/.claude/semo/):"));
1193
+ console.log(chalk_1.default.white.bold('글로벌 설정 (~/.claude/semo/):'));
1155
1194
  const globalChecks = [
1156
- { name: "~/.claude/semo/.env", path: path.join(home, ".claude", "semo", ".env") },
1157
- { name: "~/.claude/semo/SOUL.md", path: path.join(home, ".claude", "semo", "SOUL.md") },
1158
- { name: "~/.claude/skills/", path: path.join(home, ".claude", "skills") },
1159
- { name: "~/.claude/commands/", path: path.join(home, ".claude", "commands") },
1160
- { name: "~/.claude/agents/", path: path.join(home, ".claude", "agents") },
1195
+ { name: '~/.claude/semo/.env', path: path.join(home, '.claude', 'semo', '.env') },
1196
+ { name: '~/.claude/semo/SOUL.md', path: path.join(home, '.claude', 'semo', 'SOUL.md') },
1197
+ { name: '~/.claude/skills/', path: path.join(home, '.claude', 'skills') },
1198
+ { name: '~/.claude/commands/', path: path.join(home, '.claude', 'commands') },
1199
+ { name: '~/.claude/agents/', path: path.join(home, '.claude', 'agents') },
1161
1200
  ];
1162
1201
  let globalOk = true;
1163
1202
  for (const check of globalChecks) {
1164
1203
  const exists = fs.existsSync(check.path);
1165
- console.log(` ${exists ? chalk_1.default.green("") : chalk_1.default.red("")} ${check.name}`);
1204
+ console.log(` ${exists ? chalk_1.default.green('') : chalk_1.default.red('')} ${check.name}`);
1166
1205
  if (!exists)
1167
1206
  globalOk = false;
1168
1207
  }
1169
1208
  // DB 연결 확인
1170
- console.log(chalk_1.default.white.bold("\nDB 연결:"));
1209
+ console.log(chalk_1.default.white.bold('\nDB 연결:'));
1171
1210
  const connected = await (0, database_1.isDbConnected)();
1172
1211
  if (connected) {
1173
- console.log(chalk_1.default.green(" ✓ DB 연결 정상"));
1212
+ console.log(chalk_1.default.green(' ✓ DB 연결 정상'));
1174
1213
  }
1175
1214
  else {
1176
- console.log(chalk_1.default.red(" ✗ DB 연결 실패"));
1215
+ console.log(chalk_1.default.red(' ✗ DB 연결 실패'));
1177
1216
  globalOk = false;
1178
1217
  }
1179
1218
  await (0, database_1.closeConnection)();
1180
1219
  console.log();
1181
1220
  if (globalOk) {
1182
- console.log(chalk_1.default.green.bold("SEMO가 정상적으로 설치되어 있습니다."));
1221
+ console.log(chalk_1.default.green.bold('SEMO가 정상적으로 설치되어 있습니다.'));
1183
1222
  }
1184
1223
  else {
1185
1224
  console.log(chalk_1.default.yellow("일부 구성 요소가 누락되었습니다. 'semo onboarding'을 실행하세요."));
@@ -1188,42 +1227,42 @@ program
1188
1227
  });
1189
1228
  // === update 명령어 ===
1190
1229
  program
1191
- .command("update")
1192
- .description("SEMO를 최신 버전으로 업데이트합니다")
1193
- .option("--self", "CLI만 업데이트")
1194
- .option("--global", "글로벌 스킬/커맨드/에이전트를 DB 최신으로 갱신 (~/.claude/)")
1230
+ .command('update')
1231
+ .description('SEMO를 최신 버전으로 업데이트합니다')
1232
+ .option('--self', 'CLI만 업데이트')
1233
+ .option('--global', '글로벌 스킬/커맨드/에이전트를 DB 최신으로 갱신 (~/.claude/)')
1195
1234
  .action(async (options) => {
1196
1235
  // === --self: CLI만 업데이트 ===
1197
1236
  if (options.self) {
1198
- console.log(chalk_1.default.cyan.bold("\n🔄 SEMO CLI 업데이트\n"));
1237
+ console.log(chalk_1.default.cyan.bold('\n🔄 SEMO CLI 업데이트\n'));
1199
1238
  await showVersionComparison();
1200
- const cliSpinner = (0, ora_1.default)(" @team-semicolon/semo-cli 업데이트 중...").start();
1239
+ const cliSpinner = (0, ora_1.default)(' @team-semicolon/semo-cli 업데이트 중...').start();
1201
1240
  try {
1202
- (0, child_process_1.execSync)("npm update -g @team-semicolon/semo-cli", { stdio: "pipe" });
1203
- cliSpinner.succeed(" CLI 업데이트 완료");
1241
+ (0, child_process_1.execSync)('npm update -g @team-semicolon/semo-cli', { stdio: 'pipe' });
1242
+ cliSpinner.succeed(' CLI 업데이트 완료');
1204
1243
  }
1205
1244
  catch (error) {
1206
- cliSpinner.fail(" CLI 업데이트 실패");
1245
+ cliSpinner.fail(' CLI 업데이트 실패');
1207
1246
  const errorMsg = String(error);
1208
- if (errorMsg.includes("EACCES") || errorMsg.includes("permission")) {
1209
- console.log(chalk_1.default.yellow("\n 💡 권한 오류: 다음 명령어로 재시도하세요:"));
1210
- console.log(chalk_1.default.white(" sudo npm update -g @team-semicolon/semo-cli\n"));
1247
+ if (errorMsg.includes('EACCES') || errorMsg.includes('permission')) {
1248
+ console.log(chalk_1.default.yellow('\n 💡 권한 오류: 다음 명령어로 재시도하세요:'));
1249
+ console.log(chalk_1.default.white(' sudo npm update -g @team-semicolon/semo-cli\n'));
1211
1250
  }
1212
1251
  else {
1213
1252
  console.error(chalk_1.default.gray(` ${errorMsg}`));
1214
1253
  }
1215
1254
  }
1216
- console.log(chalk_1.default.green.bold("\n✅ CLI 업데이트 완료!\n"));
1255
+ console.log(chalk_1.default.green.bold('\n✅ CLI 업데이트 완료!\n'));
1217
1256
  return;
1218
1257
  }
1219
1258
  // === --global 또는 기본: DB 기반 글로벌 갱신 ===
1220
- console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 업데이트\n"));
1259
+ console.log(chalk_1.default.cyan.bold('\n🔄 SEMO 업데이트\n'));
1221
1260
  // 1. 버전 비교 (CLI only)
1222
1261
  await showVersionComparison();
1223
1262
  // 2. DB 연결 확인
1224
1263
  const connected = await (0, database_1.isDbConnected)();
1225
1264
  if (!connected) {
1226
- console.log(chalk_1.default.red(" DB 연결 실패 — ~/.claude/semo/.env를 확인하세요."));
1265
+ console.log(chalk_1.default.red(' DB 연결 실패 — ~/.claude/semo/.env를 확인하세요.'));
1227
1266
  await (0, database_1.closeConnection)();
1228
1267
  process.exit(1);
1229
1268
  }
@@ -1232,90 +1271,90 @@ program
1232
1271
  // 4. Hooks 업데이트
1233
1272
  await setupHooks(true);
1234
1273
  await (0, database_1.closeConnection)();
1235
- console.log(chalk_1.default.green.bold("\n✅ SEMO 업데이트 완료!\n"));
1236
- console.log(chalk_1.default.gray(" 💡 전체 재설치가 필요하면: semo onboarding -f\n"));
1274
+ console.log(chalk_1.default.green.bold('\n✅ SEMO 업데이트 완료!\n'));
1275
+ console.log(chalk_1.default.gray(' 💡 전체 재설치가 필요하면: semo onboarding -f\n'));
1237
1276
  });
1238
1277
  // === migrate 명령어 (deprecated) ===
1239
1278
  program
1240
- .command("migrate")
1241
- .description("[deprecated] semo-system 마이그레이션은 더 이상 필요하지 않습니다")
1279
+ .command('migrate')
1280
+ .description('[deprecated] semo-system 마이그레이션은 더 이상 필요하지 않습니다')
1242
1281
  .action(async () => {
1243
1282
  console.log(chalk_1.default.yellow("\n⚠ 'semo migrate'는 더 이상 필요하지 않습니다.\n"));
1244
- console.log(chalk_1.default.gray(" SEMO는 이제 DB 기반으로 동작하며, semo-system/ 의존성이 제거되었습니다."));
1245
- console.log(chalk_1.default.cyan("\n 전체 재설치가 필요하면:"));
1246
- console.log(chalk_1.default.gray(" semo onboarding -f\n"));
1283
+ console.log(chalk_1.default.gray(' SEMO는 이제 DB 기반으로 동작하며, semo-system/ 의존성이 제거되었습니다.'));
1284
+ console.log(chalk_1.default.cyan('\n 전체 재설치가 필요하면:'));
1285
+ console.log(chalk_1.default.gray(' semo onboarding -f\n'));
1247
1286
  });
1248
1287
  // === config 명령어 (설치 후 설정 변경) ===
1249
- const configCmd = program.command("config").description("SEMO 설정 관리");
1288
+ const configCmd = program.command('config').description('SEMO 설정 관리');
1250
1289
  configCmd
1251
- .command("env")
1252
- .description("SEMO 환경변수 설정 (DATABASE_URL, OPENAI_API_KEY 등)")
1253
- .option("--credentials-gist <gistId>", "Private GitHub Gist에서 자동 가져오기")
1254
- .option("--force", "기존 값도 Gist에서 덮어쓰기")
1290
+ .command('env')
1291
+ .description('SEMO 환경변수 설정 (DATABASE_URL, OPENAI_API_KEY 등)')
1292
+ .option('--credentials-gist <gistId>', 'Private GitHub Gist에서 자동 가져오기')
1293
+ .option('--force', '기존 값도 Gist에서 덮어쓰기')
1255
1294
  .action(async (options) => {
1256
- console.log(chalk_1.default.cyan.bold("\n🔑 SEMO 환경변수 설정\n"));
1295
+ console.log(chalk_1.default.cyan.bold('\n🔑 SEMO 환경변수 설정\n'));
1257
1296
  const existing = readSemoEnvCreds();
1258
1297
  const hasExisting = Object.keys(existing).length > 0;
1259
1298
  if (hasExisting && !options.force) {
1260
- const keys = Object.keys(existing).join(", ");
1299
+ const keys = Object.keys(existing).join(', ');
1261
1300
  const { overwrite } = await inquirer_1.default.prompt([
1262
1301
  {
1263
- type: "confirm",
1264
- name: "overwrite",
1302
+ type: 'confirm',
1303
+ name: 'overwrite',
1265
1304
  message: `기존 설정이 있습니다 (${keys}). Gist에서 없는 키를 보충하시겠습니까?`,
1266
1305
  default: true,
1267
1306
  },
1268
1307
  ]);
1269
1308
  if (!overwrite) {
1270
- console.log(chalk_1.default.gray("취소됨"));
1309
+ console.log(chalk_1.default.gray('취소됨'));
1271
1310
  return;
1272
1311
  }
1273
1312
  }
1274
1313
  await setupSemoEnv(options.credentialsGist, options.force);
1275
- console.log(chalk_1.default.gray(" 다음 Claude Code 세션부터 자동으로 적용됩니다."));
1314
+ console.log(chalk_1.default.gray(' 다음 Claude Code 세션부터 자동으로 적용됩니다.'));
1276
1315
  });
1277
1316
  // === doctor 명령어 (설치 상태 진단) ===
1278
1317
  program
1279
- .command("doctor")
1280
- .description("SEMO 설치 상태를 진단하고 문제를 리포트")
1318
+ .command('doctor')
1319
+ .description('SEMO 설치 상태를 진단하고 문제를 리포트')
1281
1320
  .action(async () => {
1282
- console.log(chalk_1.default.cyan.bold("\n🩺 SEMO 진단\n"));
1321
+ console.log(chalk_1.default.cyan.bold('\n🩺 SEMO 진단\n'));
1283
1322
  const home = os.homedir();
1284
1323
  const cwd = process.cwd();
1285
1324
  // 1. 레거시 환경 확인
1286
- console.log(chalk_1.default.cyan("1. 레거시 환경 확인"));
1325
+ console.log(chalk_1.default.cyan('1. 레거시 환경 확인'));
1287
1326
  const legacyCheck = detectLegacyEnvironment(cwd);
1288
1327
  if (legacyCheck.hasLegacy) {
1289
- console.log(chalk_1.default.yellow(" ⚠️ 레거시 환경 감지됨"));
1290
- legacyCheck.legacyPaths.forEach(p => {
1328
+ console.log(chalk_1.default.yellow(' ⚠️ 레거시 환경 감지됨'));
1329
+ legacyCheck.legacyPaths.forEach((p) => {
1291
1330
  console.log(chalk_1.default.gray(` - ${p}`));
1292
1331
  });
1293
- console.log(chalk_1.default.gray(" 💡 레거시 폴더를 수동 삭제하세요 (semo-system/, semo-core/ 등)"));
1332
+ console.log(chalk_1.default.gray(' 💡 레거시 폴더를 수동 삭제하세요 (semo-system/, semo-core/ 등)'));
1294
1333
  }
1295
1334
  else {
1296
- console.log(chalk_1.default.green(" ✅ 레거시 환경 없음"));
1335
+ console.log(chalk_1.default.green(' ✅ 레거시 환경 없음'));
1297
1336
  }
1298
1337
  // 2. DB 연결 확인
1299
- console.log(chalk_1.default.cyan("\n2. DB 연결"));
1338
+ console.log(chalk_1.default.cyan('\n2. DB 연결'));
1300
1339
  const connected = await (0, database_1.isDbConnected)();
1301
1340
  if (connected) {
1302
- console.log(chalk_1.default.green(" ✅ DB 연결 정상"));
1341
+ console.log(chalk_1.default.green(' ✅ DB 연결 정상'));
1303
1342
  }
1304
1343
  else {
1305
- console.log(chalk_1.default.red(" ❌ DB 연결 실패"));
1306
- console.log(chalk_1.default.gray(" 💡 흔한 원인:"));
1307
- console.log(chalk_1.default.gray(" - SSH 터널 미실행 (로컬 개발 시 필수)"));
1308
- console.log(chalk_1.default.gray(" - ~/.claude/semo/.env의 DATABASE_URL 오류"));
1309
- console.log(chalk_1.default.gray(" 💡 해결: SSH 터널 실행 후 semo onboarding 재시도"));
1344
+ console.log(chalk_1.default.red(' ❌ DB 연결 실패'));
1345
+ console.log(chalk_1.default.gray(' 💡 흔한 원인:'));
1346
+ console.log(chalk_1.default.gray(' - SSH 터널 미실행 (로컬 개발 시 필수)'));
1347
+ console.log(chalk_1.default.gray(' - ~/.claude/semo/.env의 DATABASE_URL 오류'));
1348
+ console.log(chalk_1.default.gray(' 💡 해결: SSH 터널 실행 후 semo onboarding 재시도'));
1310
1349
  }
1311
1350
  // 3. 글로벌 설정 확인
1312
- console.log(chalk_1.default.cyan("\n3. 글로벌 설정 (~/.claude/semo/)"));
1351
+ console.log(chalk_1.default.cyan('\n3. 글로벌 설정 (~/.claude/semo/)'));
1313
1352
  const globalChecks = [
1314
- { name: ".env", path: path.join(home, ".claude", "semo", ".env") },
1315
- { name: "SOUL.md", path: path.join(home, ".claude", "semo", "SOUL.md") },
1316
- { name: "skills/", path: path.join(home, ".claude", "skills") },
1317
- { name: "commands/", path: path.join(home, ".claude", "commands") },
1318
- { name: "agents/", path: path.join(home, ".claude", "agents") },
1353
+ { name: '.env', path: path.join(home, '.claude', 'semo', '.env') },
1354
+ { name: 'SOUL.md', path: path.join(home, '.claude', 'semo', 'SOUL.md') },
1355
+ { name: 'skills/', path: path.join(home, '.claude', 'skills') },
1356
+ { name: 'commands/', path: path.join(home, '.claude', 'commands') },
1357
+ { name: 'agents/', path: path.join(home, '.claude', 'agents') },
1319
1358
  ];
1320
1359
  for (const check of globalChecks) {
1321
1360
  const exists = fs.existsSync(check.path);
@@ -1333,24 +1372,26 @@ program
1333
1372
  const kb_1 = require("./kb");
1334
1373
  // Re-implement readSyncState locally (simple file read)
1335
1374
  function readSyncState(cwd) {
1336
- const statePath = path.join(cwd, ".kb", ".sync-state.json");
1375
+ const statePath = path.join(cwd, '.kb', '.sync-state.json');
1337
1376
  if (fs.existsSync(statePath)) {
1338
1377
  try {
1339
- return JSON.parse(fs.readFileSync(statePath, "utf-8"));
1378
+ return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
1379
+ }
1380
+ catch {
1381
+ /* */
1340
1382
  }
1341
- catch { /* */ }
1342
1383
  }
1343
1384
  return { lastPull: null, lastPush: null, sharedCount: 0 };
1344
1385
  }
1345
1386
  const kbCmd = program
1346
- .command("kb")
1347
- .description("KB(Knowledge Base) 관리 — SEMO DB 기반 지식 저장소");
1387
+ .command('kb')
1388
+ .description('KB(Knowledge Base) 관리 — SEMO DB 기반 지식 저장소');
1348
1389
  kbCmd
1349
- .command("pull")
1350
- .description("DB에서 KB를 로컬 .kb/로 내려받기")
1351
- .option("--domain <name>", "특정 도메인만")
1390
+ .command('pull')
1391
+ .description('DB에서 KB를 로컬 .kb/로 내려받기')
1392
+ .option('--domain <name>', '특정 도메인만')
1352
1393
  .action(async (options) => {
1353
- const spinner = (0, ora_1.default)("KB 데이터 가져오는 중...").start();
1394
+ const spinner = (0, ora_1.default)('KB 데이터 가져오는 중...').start();
1354
1395
  try {
1355
1396
  const pool = (0, database_1.getPool)();
1356
1397
  const result = await (0, kb_1.kbPull)(pool, options.domain, process.cwd());
@@ -1369,18 +1410,18 @@ kbCmd
1369
1410
  }
1370
1411
  });
1371
1412
  kbCmd
1372
- .command("push")
1373
- .description("로컬 .kb/ 데이터를 DB에 업로드")
1374
- .option("--file <path>", ".kb/ 내 특정 파일", "team.json")
1375
- .option("--created-by <name>", "작성자 식별자")
1413
+ .command('push')
1414
+ .description('로컬 .kb/ 데이터를 DB에 업로드')
1415
+ .option('--file <path>', '.kb/ 내 특정 파일', 'team.json')
1416
+ .option('--created-by <name>', '작성자 식별자')
1376
1417
  .action(async (options) => {
1377
1418
  const cwd = process.cwd();
1378
- const kbDir = path.join(cwd, ".kb");
1419
+ const kbDir = path.join(cwd, '.kb');
1379
1420
  if (!fs.existsSync(kbDir)) {
1380
- console.log(chalk_1.default.red("❌ .kb/ 디렉토리가 없습니다. 먼저 semo kb pull을 실행하세요."));
1421
+ console.log(chalk_1.default.red('❌ .kb/ 디렉토리가 없습니다. 먼저 semo kb pull을 실행하세요.'));
1381
1422
  process.exit(1);
1382
1423
  }
1383
- const spinner = (0, ora_1.default)("KB 데이터 업로드 중...").start();
1424
+ const spinner = (0, ora_1.default)('KB 데이터 업로드 중...').start();
1384
1425
  try {
1385
1426
  const pool = (0, database_1.getPool)();
1386
1427
  const filePath = path.join(kbDir, options.file);
@@ -1388,13 +1429,13 @@ kbCmd
1388
1429
  spinner.fail(`파일을 찾을 수 없습니다: .kb/${options.file}`);
1389
1430
  process.exit(1);
1390
1431
  }
1391
- const entries = JSON.parse(fs.readFileSync(filePath, "utf-8"));
1432
+ const entries = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1392
1433
  const result = await (0, kb_1.kbPush)(pool, entries, options.createdBy, cwd);
1393
1434
  spinner.succeed(`KB push 완료`);
1394
1435
  console.log(chalk_1.default.green(` ✅ ${result.upserted}건 업서트됨`));
1395
1436
  if (result.errors.length > 0) {
1396
1437
  console.log(chalk_1.default.yellow(` ⚠️ ${result.errors.length}건 오류:`));
1397
- result.errors.forEach(e => console.log(chalk_1.default.red(` ${e}`)));
1438
+ result.errors.forEach((e) => console.log(chalk_1.default.red(` ${e}`)));
1398
1439
  }
1399
1440
  await (0, database_1.closeConnection)();
1400
1441
  }
@@ -1405,16 +1446,16 @@ kbCmd
1405
1446
  }
1406
1447
  });
1407
1448
  kbCmd
1408
- .command("status")
1409
- .description("KB 동기화 상태 확인")
1449
+ .command('status')
1450
+ .description('KB 동기화 상태 확인')
1410
1451
  .action(async () => {
1411
- const spinner = (0, ora_1.default)("KB 상태 조회 중...").start();
1452
+ const spinner = (0, ora_1.default)('KB 상태 조회 중...').start();
1412
1453
  try {
1413
1454
  const pool = (0, database_1.getPool)();
1414
1455
  const status = await (0, kb_1.kbStatus)(pool);
1415
1456
  spinner.stop();
1416
- console.log(chalk_1.default.cyan.bold("\n📊 KB 상태\n"));
1417
- console.log(chalk_1.default.white(" 📦 KB (knowledge_base)"));
1457
+ console.log(chalk_1.default.cyan.bold('\n📊 KB 상태\n'));
1458
+ console.log(chalk_1.default.white(' 📦 KB (knowledge_base)'));
1418
1459
  console.log(chalk_1.default.gray(` 총 ${status.shared.total}건`));
1419
1460
  for (const [domain, count] of Object.entries(status.shared.domains)) {
1420
1461
  console.log(chalk_1.default.gray(` - ${domain}: ${count}건`));
@@ -1425,7 +1466,7 @@ kbCmd
1425
1466
  // Local sync state
1426
1467
  const syncState = readSyncState(process.cwd());
1427
1468
  if (syncState.lastPull || syncState.lastPush) {
1428
- console.log(chalk_1.default.white("\n 🔄 로컬 동기화"));
1469
+ console.log(chalk_1.default.white('\n 🔄 로컬 동기화'));
1429
1470
  if (syncState.lastPull)
1430
1471
  console.log(chalk_1.default.gray(` 마지막 pull: ${syncState.lastPull}`));
1431
1472
  if (syncState.lastPush)
@@ -1441,12 +1482,12 @@ kbCmd
1441
1482
  }
1442
1483
  });
1443
1484
  kbCmd
1444
- .command("list")
1445
- .description("KB 항목 목록 조회")
1446
- .option("--domain <name>", "도메인 필터")
1447
- .option("--service <name>", "서비스(프로젝트) 필터 — 해당 서비스의 모든 도메인 항목 반환")
1448
- .option("--limit <n>", "최대 항목 수", "50")
1449
- .option("--format <type>", "출력 형식 (table|json)", "table")
1485
+ .command('list')
1486
+ .description('KB 항목 목록 조회')
1487
+ .option('--domain <name>', '도메인 필터')
1488
+ .option('--service <name>', '서비스(프로젝트) 필터 — 해당 서비스의 모든 도메인 항목 반환')
1489
+ .option('--limit <n>', '최대 항목 수', '50')
1490
+ .option('--format <type>', '출력 형식 (table|json)', 'table')
1450
1491
  .action(async (options) => {
1451
1492
  try {
1452
1493
  const pool = (0, database_1.getPool)();
@@ -1455,21 +1496,21 @@ kbCmd
1455
1496
  service: options.service,
1456
1497
  limit: parseInt(options.limit),
1457
1498
  });
1458
- if (options.format === "json") {
1499
+ if (options.format === 'json') {
1459
1500
  console.log(JSON.stringify(entries, null, 2));
1460
1501
  }
1461
1502
  else {
1462
- console.log(chalk_1.default.cyan.bold("\n📋 KB 목록\n"));
1503
+ console.log(chalk_1.default.cyan.bold('\n📋 KB 목록\n'));
1463
1504
  if (entries.length > 0) {
1464
- console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
1505
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────'));
1465
1506
  for (const entry of entries) {
1466
- const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
1507
+ const preview = entry.content.substring(0, 60).replace(/\n/g, ' ');
1467
1508
  console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
1468
- console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
1509
+ console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? '...' : ''}`));
1469
1510
  }
1470
1511
  }
1471
1512
  else {
1472
- console.log(chalk_1.default.yellow(" KB가 비어있습니다."));
1513
+ console.log(chalk_1.default.yellow(' KB가 비어있습니다.'));
1473
1514
  }
1474
1515
  console.log();
1475
1516
  }
@@ -1482,13 +1523,13 @@ kbCmd
1482
1523
  }
1483
1524
  });
1484
1525
  kbCmd
1485
- .command("search <query>")
1486
- .description("KB 검색 (시맨틱 + 텍스트 하이브리드)")
1487
- .option("--domain <name>", "도메인 필터")
1488
- .option("--service <name>", "서비스(프로젝트) 필터")
1489
- .option("--limit <n>", "최대 결과 수", "10")
1490
- .option("--mode <type>", "검색 모드 (hybrid|semantic|text)", "hybrid")
1491
- .option("--short", "미리보기 모드 (content를 80자로 잘라서 표시)")
1526
+ .command('search <query>')
1527
+ .description('KB 검색 (시맨틱 + 텍스트 하이브리드)')
1528
+ .option('--domain <name>', '도메인 필터')
1529
+ .option('--service <name>', '서비스(프로젝트) 필터')
1530
+ .option('--limit <n>', '최대 결과 수', '10')
1531
+ .option('--mode <type>', '검색 모드 (hybrid|semantic|text)', 'hybrid')
1532
+ .option('--short', '미리보기 모드 (content를 80자로 잘라서 표시)')
1492
1533
  .action(async (query, options) => {
1493
1534
  const spinner = (0, ora_1.default)(`'${query}' 검색 중...`).start();
1494
1535
  try {
@@ -1507,12 +1548,12 @@ kbCmd
1507
1548
  console.log(chalk_1.default.cyan.bold(`\n🔍 검색 결과: '${query}' (${results.length}건)\n`));
1508
1549
  for (const entry of results) {
1509
1550
  const score = entry.score;
1510
- const scoreStr = score ? chalk_1.default.yellow(` (${(score * 100).toFixed(1)}%)`) : "";
1551
+ const scoreStr = score ? chalk_1.default.yellow(` (${(score * 100).toFixed(1)}%)`) : '';
1511
1552
  const fullKey = entry.sub_key ? `${entry.key}/${entry.sub_key}` : entry.key;
1512
1553
  console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(fullKey) + scoreStr);
1513
1554
  if (options.short) {
1514
- const preview = entry.content.substring(0, 80).replace(/\n/g, " ");
1515
- console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 80 ? "..." : ""}`));
1555
+ const preview = entry.content.substring(0, 80).replace(/\n/g, ' ');
1556
+ console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 80 ? '...' : ''}`));
1516
1557
  }
1517
1558
  else {
1518
1559
  console.log(chalk_1.default.gray(` ${entry.content}`));
@@ -1529,25 +1570,25 @@ kbCmd
1529
1570
  }
1530
1571
  });
1531
1572
  kbCmd
1532
- .command("embed")
1533
- .description("기존 KB 항목에 임베딩 벡터 생성 (OPENAI_API_KEY 필요)")
1534
- .option("--domain <name>", "도메인 필터")
1535
- .option("--force", "이미 임베딩된 항목도 재생성")
1573
+ .command('embed')
1574
+ .description('기존 KB 항목에 임베딩 벡터 생성 (OPENAI_API_KEY 필요)')
1575
+ .option('--domain <name>', '도메인 필터')
1576
+ .option('--force', '이미 임베딩된 항목도 재생성')
1536
1577
  .action(async (options) => {
1537
1578
  if (!process.env.OPENAI_API_KEY) {
1538
- console.log(chalk_1.default.red("❌ OPENAI_API_KEY 환경변수가 설정되지 않았습니다."));
1579
+ console.log(chalk_1.default.red('❌ OPENAI_API_KEY 환경변수가 설정되지 않았습니다.'));
1539
1580
  console.log(chalk_1.default.gray(" export OPENAI_API_KEY='sk-...'"));
1540
1581
  process.exit(1);
1541
1582
  }
1542
- const spinner = (0, ora_1.default)("임베딩 대상 조회 중...").start();
1583
+ const spinner = (0, ora_1.default)('임베딩 대상 조회 중...').start();
1543
1584
  try {
1544
1585
  const pool = (0, database_1.getPool)();
1545
1586
  const client = await pool.connect();
1546
- let sql = "SELECT kb_id, domain, key, content FROM semo.knowledge_base WHERE 1=1";
1587
+ let sql = 'SELECT kb_id, domain, key, content FROM semo.knowledge_base WHERE 1=1';
1547
1588
  const params = [];
1548
1589
  let pIdx = 1;
1549
1590
  if (!options.force)
1550
- sql += " AND embedding IS NULL";
1591
+ sql += ' AND embedding IS NULL';
1551
1592
  if (options.domain) {
1552
1593
  sql += ` AND domain = $${pIdx++}`;
1553
1594
  params.push(options.domain);
@@ -1556,7 +1597,7 @@ kbCmd
1556
1597
  const total = rows.rows.length;
1557
1598
  spinner.succeed(`${total}건 임베딩 대상`);
1558
1599
  if (total === 0) {
1559
- console.log(chalk_1.default.green(" 모든 항목이 이미 임베딩되어 있습니다."));
1600
+ console.log(chalk_1.default.green(' 모든 항목이 이미 임베딩되어 있습니다.'));
1560
1601
  client.release();
1561
1602
  await (0, database_1.closeConnection)();
1562
1603
  return;
@@ -1566,7 +1607,7 @@ kbCmd
1566
1607
  for (const row of rows.rows) {
1567
1608
  const embedding = await (0, kb_1.generateEmbedding)(`${row.key}: ${row.content}`);
1568
1609
  if (embedding) {
1569
- await client.query("UPDATE semo.knowledge_base SET embedding = $1::vector WHERE kb_id = $2", [`[${embedding.join(",")}]`, row.kb_id]);
1610
+ await client.query('UPDATE semo.knowledge_base SET embedding = $1::vector WHERE kb_id = $2', [`[${embedding.join(',')}]`, row.kb_id]);
1570
1611
  }
1571
1612
  done++;
1572
1613
  embedSpinner.text = `임베딩 생성 중... ${done}/${total}`;
@@ -1582,23 +1623,23 @@ kbCmd
1582
1623
  }
1583
1624
  });
1584
1625
  kbCmd
1585
- .command("sync")
1586
- .description("양방향 동기화 (pull → merge → push)")
1587
- .option("--domain <name>", "도메인 필터")
1626
+ .command('sync')
1627
+ .description('양방향 동기화 (pull → merge → push)')
1628
+ .option('--domain <name>', '도메인 필터')
1588
1629
  .action(async (options) => {
1589
- console.log(chalk_1.default.cyan.bold("\n🔄 KB 동기화\n"));
1590
- const spinner = (0, ora_1.default)("Step 1/2: DB에서 pull...").start();
1630
+ console.log(chalk_1.default.cyan.bold('\n🔄 KB 동기화\n'));
1631
+ const spinner = (0, ora_1.default)('Step 1/2: DB에서 pull...').start();
1591
1632
  try {
1592
1633
  const pool = (0, database_1.getPool)();
1593
1634
  // Step 1: Pull
1594
1635
  const pulled = await (0, kb_1.kbPull)(pool, options.domain, process.cwd());
1595
1636
  spinner.succeed(`Pull 완료: ${pulled.length}건`);
1596
1637
  // Step 2: Pull ontology
1597
- const spinner2 = (0, ora_1.default)("Step 2/2: 온톨로지 동기화...").start();
1638
+ const spinner2 = (0, ora_1.default)('Step 2/2: 온톨로지 동기화...').start();
1598
1639
  const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
1599
1640
  spinner2.succeed(`온톨로지 ${ontoCount}개 도메인 동기화됨`);
1600
- console.log(chalk_1.default.green.bold("\n✅ 동기화 완료\n"));
1601
- console.log(chalk_1.default.gray(" 로컬 수정 후 semo kb push로 업로드하세요."));
1641
+ console.log(chalk_1.default.green.bold('\n✅ 동기화 완료\n'));
1642
+ console.log(chalk_1.default.gray(' 로컬 수정 후 semo kb push로 업로드하세요.'));
1602
1643
  console.log();
1603
1644
  await (0, database_1.closeConnection)();
1604
1645
  }
@@ -1609,9 +1650,9 @@ kbCmd
1609
1650
  }
1610
1651
  });
1611
1652
  kbCmd
1612
- .command("get <domain> <key> [sub_key]")
1613
- .description("KB 단일 항목 정확 조회 (domain + key + sub_key)")
1614
- .option("--format <type>", "출력 형식 (json|table)", "json")
1653
+ .command('get <domain> <key> [sub_key]')
1654
+ .description('KB 단일 항목 정확 조회 (domain + key + sub_key)')
1655
+ .option('--format <type>', '출력 형식 (json|table)', 'json')
1615
1656
  .action(async (domain, key, subKey, options) => {
1616
1657
  try {
1617
1658
  const pool = (0, database_1.getPool)();
@@ -1621,7 +1662,7 @@ kbCmd
1621
1662
  await (0, database_1.closeConnection)();
1622
1663
  process.exit(1);
1623
1664
  }
1624
- if (options.format === "json") {
1665
+ if (options.format === 'json') {
1625
1666
  console.log(JSON.stringify(entry, null, 2));
1626
1667
  }
1627
1668
  else {
@@ -1644,13 +1685,13 @@ kbCmd
1644
1685
  }
1645
1686
  });
1646
1687
  kbCmd
1647
- .command("upsert <domain> <key> [sub_key]")
1648
- .description("KB 항목 쓰기 (upsert) — 임베딩 자동 생성 + 스키마 검증 (key는 kebab-case만 허용)")
1649
- .requiredOption("--content <text>", "항목 본문")
1650
- .option("--metadata <json>", "추가 메타데이터 (JSON 문자열)")
1651
- .option("--created-by <name>", "작성자 식별자", "semo-cli")
1688
+ .command('upsert <domain> <key> [sub_key]')
1689
+ .description('KB 항목 쓰기 (upsert) — 임베딩 자동 생성 + 스키마 검증 (key는 kebab-case만 허용)')
1690
+ .requiredOption('--content <text>', '항목 본문')
1691
+ .option('--metadata <json>', '추가 메타데이터 (JSON 문자열)')
1692
+ .option('--created-by <name>', '작성자 식별자', 'semo-cli')
1652
1693
  .action(async (domain, key, subKey, options) => {
1653
- const spinner = (0, ora_1.default)("KB upsert 중...").start();
1694
+ const spinner = (0, ora_1.default)('KB upsert 중...').start();
1654
1695
  try {
1655
1696
  const pool = (0, database_1.getPool)();
1656
1697
  const metadata = options.metadata ? JSON.parse(options.metadata) : undefined;
@@ -1683,9 +1724,9 @@ kbCmd
1683
1724
  }
1684
1725
  });
1685
1726
  kbCmd
1686
- .command("delete <domain> <key> [sub_key]")
1687
- .description("KB 항목 삭제 (domain + key + sub_key)")
1688
- .option("--yes", "확인 프롬프트 건너뛰기 (크론/스크립트용)")
1727
+ .command('delete <domain> <key> [sub_key]')
1728
+ .description('KB 항목 삭제 (domain + key + sub_key)')
1729
+ .option('--yes', '확인 프롬프트 건너뛰기 (크론/스크립트용)')
1689
1730
  .action(async (domain, key, subKey, options) => {
1690
1731
  try {
1691
1732
  const pool = (0, database_1.getPool)();
@@ -1702,14 +1743,14 @@ kbCmd
1702
1743
  console.log(chalk_1.default.cyan(`\n📄 삭제 대상: ${fullPath}`));
1703
1744
  console.log(chalk_1.default.gray(` version: ${entry.version} | created_by: ${entry.created_by} | updated: ${entry.updated_at}`));
1704
1745
  console.log(chalk_1.default.gray(` content: ${entry.content.substring(0, 120)}${entry.content.length > 120 ? '...' : ''}\n`));
1705
- const { createInterface } = await import("readline");
1746
+ const { createInterface } = await import('readline');
1706
1747
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1707
1748
  const answer = await new Promise((resolve) => {
1708
- rl.question(chalk_1.default.yellow(" 정말 삭제하시겠습니까? (y/N): "), resolve);
1749
+ rl.question(chalk_1.default.yellow(' 정말 삭제하시겠습니까? (y/N): '), resolve);
1709
1750
  });
1710
1751
  rl.close();
1711
- if (answer.toLowerCase() !== "y") {
1712
- console.log(chalk_1.default.gray(" 취소됨.\n"));
1752
+ if (answer.toLowerCase() !== 'y') {
1753
+ console.log(chalk_1.default.gray(' 취소됨.\n'));
1713
1754
  await (0, database_1.closeConnection)();
1714
1755
  return;
1715
1756
  }
@@ -1732,36 +1773,36 @@ kbCmd
1732
1773
  }
1733
1774
  });
1734
1775
  kbCmd
1735
- .command("ontology")
1736
- .description("온톨로지 조회 — 도메인/타입/스키마/라우팅 테이블")
1737
- .option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register|unregister|create-type|add-key|remove-key)", "list")
1738
- .option("--domain <name>", "action=show|register 시 도메인")
1739
- .option("--type <name>", "action=schema|register|add-key|remove-key 시 타입 키")
1740
- .option("--key <name>", "action=add-key|remove-key 시 스키마 키")
1741
- .option("--key-type <type>", "action=add-key 시 키 유형 (singleton|collection)", "singleton")
1742
- .option("--required", "action=add-key 시 필수 여부")
1743
- .option("--hint <text>", "action=add-key 시 값 힌트")
1744
- .option("--description <text>", "action=register|add-key 시 설명")
1745
- .option("--service <name>", "action=register 시 서비스 그룹")
1746
- .option("--tags <tags>", "action=register 시 태그 (쉼표 구분)")
1747
- .option("--no-init", "action=register 시 필수 KB entry 자동 생성 건너뛰기")
1748
- .option("--force", "action=unregister 시 잔존 KB 항목도 모두 삭제")
1749
- .option("--yes", "action=unregister 시 확인 프롬프트 건너뛰기")
1750
- .option("--format <type>", "출력 형식 (json|table)", "table")
1776
+ .command('ontology')
1777
+ .description('온톨로지 조회 — 도메인/타입/스키마/라우팅 테이블')
1778
+ .option('--action <type>', '동작 (list|show|services|types|instances|schema|routing-table|register|unregister|create-type|add-key|remove-key)', 'list')
1779
+ .option('--domain <name>', 'action=show|register 시 도메인')
1780
+ .option('--type <name>', 'action=schema|register|add-key|remove-key 시 타입 키')
1781
+ .option('--key <name>', 'action=add-key|remove-key 시 스키마 키')
1782
+ .option('--key-type <type>', 'action=add-key 시 키 유형 (singleton|collection)', 'singleton')
1783
+ .option('--required', 'action=add-key 시 필수 여부')
1784
+ .option('--hint <text>', 'action=add-key 시 값 힌트')
1785
+ .option('--description <text>', 'action=register|add-key 시 설명')
1786
+ .option('--service <name>', 'action=register 시 서비스 그룹')
1787
+ .option('--tags <tags>', 'action=register 시 태그 (쉼표 구분)')
1788
+ .option('--no-init', 'action=register 시 필수 KB entry 자동 생성 건너뛰기')
1789
+ .option('--force', 'action=unregister 시 잔존 KB 항목도 모두 삭제')
1790
+ .option('--yes', 'action=unregister 시 확인 프롬프트 건너뛰기')
1791
+ .option('--format <type>', '출력 형식 (json|table)', 'table')
1751
1792
  .action(async (options) => {
1752
1793
  try {
1753
1794
  const pool = (0, database_1.getPool)();
1754
1795
  const action = options.action;
1755
- if (action === "list") {
1796
+ if (action === 'list') {
1756
1797
  const domains = await (0, kb_1.ontoList)(pool);
1757
- if (options.format === "json") {
1798
+ if (options.format === 'json') {
1758
1799
  console.log(JSON.stringify(domains, null, 2));
1759
1800
  }
1760
1801
  else {
1761
- console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 도메인\n"));
1802
+ console.log(chalk_1.default.cyan.bold('\n📐 온톨로지 도메인\n'));
1762
1803
  for (const d of domains) {
1763
- const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
1764
- const svcStr = d.service ? chalk_1.default.gray(` (${d.service})`) : "";
1804
+ const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : '';
1805
+ const svcStr = d.service ? chalk_1.default.gray(` (${d.service})`) : '';
1765
1806
  console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + svcStr);
1766
1807
  if (d.description)
1767
1808
  console.log(chalk_1.default.gray(` ${d.description}`));
@@ -1769,9 +1810,9 @@ kbCmd
1769
1810
  console.log();
1770
1811
  }
1771
1812
  }
1772
- else if (action === "show") {
1813
+ else if (action === 'show') {
1773
1814
  if (!options.domain) {
1774
- console.log(chalk_1.default.red("--domain 옵션이 필요합니다."));
1815
+ console.log(chalk_1.default.red('--domain 옵션이 필요합니다.'));
1775
1816
  process.exit(1);
1776
1817
  }
1777
1818
  const onto = await (0, kb_1.ontoShow)(pool, options.domain);
@@ -1779,7 +1820,7 @@ kbCmd
1779
1820
  console.log(chalk_1.default.red(`온톨로지 '${options.domain}'을 찾을 수 없습니다.`));
1780
1821
  process.exit(1);
1781
1822
  }
1782
- if (options.format === "json") {
1823
+ if (options.format === 'json') {
1783
1824
  console.log(JSON.stringify(onto, null, 2));
1784
1825
  }
1785
1826
  else {
@@ -1788,31 +1829,34 @@ kbCmd
1788
1829
  console.log(chalk_1.default.white(` ${onto.description}`));
1789
1830
  console.log(chalk_1.default.gray(` 버전: ${onto.version}`));
1790
1831
  console.log(chalk_1.default.gray(` 스키마:\n`));
1791
- console.log(chalk_1.default.white(JSON.stringify(onto.schema, null, 2).split("\n").map(l => " " + l).join("\n")));
1832
+ console.log(chalk_1.default.white(JSON.stringify(onto.schema, null, 2)
1833
+ .split('\n')
1834
+ .map((l) => ' ' + l)
1835
+ .join('\n')));
1792
1836
  console.log();
1793
1837
  }
1794
1838
  }
1795
- else if (action === "services") {
1839
+ else if (action === 'services') {
1796
1840
  const services = await (0, kb_1.ontoListServices)(pool);
1797
- if (options.format === "json") {
1841
+ if (options.format === 'json') {
1798
1842
  console.log(JSON.stringify(services, null, 2));
1799
1843
  }
1800
1844
  else {
1801
- console.log(chalk_1.default.cyan.bold("\n📐 서비스 목록\n"));
1845
+ console.log(chalk_1.default.cyan.bold('\n📐 서비스 목록\n'));
1802
1846
  for (const s of services) {
1803
1847
  console.log(chalk_1.default.cyan(` ${s.service}`) + chalk_1.default.gray(` (${s.domain_count} domains)`));
1804
- console.log(chalk_1.default.gray(` ${s.domains.join(", ")}`));
1848
+ console.log(chalk_1.default.gray(` ${s.domains.join(', ')}`));
1805
1849
  }
1806
1850
  console.log();
1807
1851
  }
1808
1852
  }
1809
- else if (action === "types") {
1853
+ else if (action === 'types') {
1810
1854
  const types = await (0, kb_1.ontoListTypes)(pool);
1811
- if (options.format === "json") {
1855
+ if (options.format === 'json') {
1812
1856
  console.log(JSON.stringify(types, null, 2));
1813
1857
  }
1814
1858
  else {
1815
- console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 타입\n"));
1859
+ console.log(chalk_1.default.cyan.bold('\n📐 온톨로지 타입\n'));
1816
1860
  for (const t of types) {
1817
1861
  console.log(chalk_1.default.cyan(` ${t.type_key}`) + chalk_1.default.gray(` (v${t.version})`));
1818
1862
  if (t.description)
@@ -1821,31 +1865,31 @@ kbCmd
1821
1865
  console.log();
1822
1866
  }
1823
1867
  }
1824
- else if (action === "instances") {
1868
+ else if (action === 'instances') {
1825
1869
  const instances = await (0, kb_1.ontoListInstances)(pool);
1826
- if (options.format === "json") {
1870
+ if (options.format === 'json') {
1827
1871
  console.log(JSON.stringify(instances, null, 2));
1828
1872
  }
1829
1873
  else {
1830
- console.log(chalk_1.default.cyan.bold("\n📐 서비스 인스턴스\n"));
1874
+ console.log(chalk_1.default.cyan.bold('\n📐 서비스 인스턴스\n'));
1831
1875
  for (const inst of instances) {
1832
1876
  console.log(chalk_1.default.cyan(` ${inst.domain}`) + chalk_1.default.gray(` (${inst.entry_count} entries)`));
1833
1877
  if (inst.description)
1834
1878
  console.log(chalk_1.default.gray(` ${inst.description}`));
1835
1879
  if (inst.scoped_domains.length > 0) {
1836
- console.log(chalk_1.default.gray(` scoped: ${inst.scoped_domains.join(", ")}`));
1880
+ console.log(chalk_1.default.gray(` scoped: ${inst.scoped_domains.join(', ')}`));
1837
1881
  }
1838
1882
  }
1839
1883
  console.log();
1840
1884
  }
1841
1885
  }
1842
- else if (action === "schema") {
1886
+ else if (action === 'schema') {
1843
1887
  if (!options.type) {
1844
- console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type service)"));
1888
+ console.log(chalk_1.default.red('--type 옵션이 필요합니다. (예: --type service)'));
1845
1889
  process.exit(1);
1846
1890
  }
1847
1891
  const schema = await (0, kb_1.ontoListSchema)(pool, options.type);
1848
- if (options.format === "json") {
1892
+ if (options.format === 'json') {
1849
1893
  console.log(JSON.stringify(schema, null, 2));
1850
1894
  }
1851
1895
  else {
@@ -1855,7 +1899,7 @@ kbCmd
1855
1899
  }
1856
1900
  else {
1857
1901
  for (const s of schema) {
1858
- const reqStr = s.required ? chalk_1.default.red(" *") : "";
1902
+ const reqStr = s.required ? chalk_1.default.red(' *') : '';
1859
1903
  const typeStr = chalk_1.default.gray(` [${s.key_type}]`);
1860
1904
  console.log(chalk_1.default.cyan(` ${s.scheme_key}`) + typeStr + reqStr);
1861
1905
  if (s.scheme_description)
@@ -1867,18 +1911,18 @@ kbCmd
1867
1911
  console.log();
1868
1912
  }
1869
1913
  }
1870
- else if (action === "routing-table") {
1914
+ else if (action === 'routing-table') {
1871
1915
  const table = await (0, kb_1.ontoRoutingTable)(pool);
1872
- if (options.format === "json") {
1916
+ if (options.format === 'json') {
1873
1917
  console.log(JSON.stringify(table, null, 2));
1874
1918
  }
1875
1919
  else {
1876
- console.log(chalk_1.default.cyan.bold("\n📐 라우팅 테이블\n"));
1877
- let lastDomain = "";
1920
+ console.log(chalk_1.default.cyan.bold('\n📐 라우팅 테이블\n'));
1921
+ let lastDomain = '';
1878
1922
  for (const r of table) {
1879
1923
  if (r.domain !== lastDomain) {
1880
1924
  lastDomain = r.domain;
1881
- const svcStr = r.service ? chalk_1.default.gray(` (${r.service})`) : "";
1925
+ const svcStr = r.service ? chalk_1.default.gray(` (${r.service})`) : '';
1882
1926
  console.log(chalk_1.default.white.bold(`\n ${r.domain}`) + chalk_1.default.gray(` [${r.entity_type}]`) + svcStr);
1883
1927
  if (r.domain_description)
1884
1928
  console.log(chalk_1.default.gray(` ${r.domain_description}`));
@@ -1891,18 +1935,20 @@ kbCmd
1891
1935
  console.log();
1892
1936
  }
1893
1937
  }
1894
- else if (action === "register") {
1938
+ else if (action === 'register') {
1895
1939
  if (!options.domain) {
1896
- console.log(chalk_1.default.red("--domain 옵션이 필요합니다."));
1940
+ console.log(chalk_1.default.red('--domain 옵션이 필요합니다.'));
1897
1941
  process.exit(1);
1898
1942
  }
1899
1943
  if (!options.type) {
1900
- console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type service, --type team, --type person)"));
1944
+ console.log(chalk_1.default.red('--type 옵션이 필요합니다. (예: --type service, --type team, --type person)'));
1901
1945
  const types = await (0, kb_1.ontoListTypes)(pool);
1902
- console.log(chalk_1.default.gray(`사용 가능한 타입: ${types.map(t => t.type_key).join(", ")}`));
1946
+ console.log(chalk_1.default.gray(`사용 가능한 타입: ${types.map((t) => t.type_key).join(', ')}`));
1903
1947
  process.exit(1);
1904
1948
  }
1905
- const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : undefined;
1949
+ const tags = options.tags
1950
+ ? options.tags.split(',').map((t) => t.trim())
1951
+ : undefined;
1906
1952
  const result = await (0, kb_1.ontoRegister)(pool, {
1907
1953
  domain: options.domain,
1908
1954
  entity_type: options.type,
@@ -1911,7 +1957,7 @@ kbCmd
1911
1957
  tags,
1912
1958
  init_required: options.init !== false,
1913
1959
  });
1914
- if (options.format === "json") {
1960
+ if (options.format === 'json') {
1915
1961
  console.log(JSON.stringify(result, null, 2));
1916
1962
  }
1917
1963
  else {
@@ -1931,9 +1977,9 @@ kbCmd
1931
1977
  }
1932
1978
  }
1933
1979
  }
1934
- else if (action === "create-type") {
1980
+ else if (action === 'create-type') {
1935
1981
  if (!options.type) {
1936
- console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type project)"));
1982
+ console.log(chalk_1.default.red('--type 옵션이 필요합니다. (예: --type project)'));
1937
1983
  process.exit(1);
1938
1984
  }
1939
1985
  const result = await (0, kb_1.ontoCreateType)(pool, {
@@ -1949,13 +1995,13 @@ kbCmd
1949
1995
  process.exit(1);
1950
1996
  }
1951
1997
  }
1952
- else if (action === "add-key") {
1998
+ else if (action === 'add-key') {
1953
1999
  if (!options.type) {
1954
- console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type service)"));
2000
+ console.log(chalk_1.default.red('--type 옵션이 필요합니다. (예: --type service)'));
1955
2001
  process.exit(1);
1956
2002
  }
1957
2003
  if (!options.key) {
1958
- console.log(chalk_1.default.red("--key 옵션이 필요합니다. (예: --key slack_channel)"));
2004
+ console.log(chalk_1.default.red('--key 옵션이 필요합니다. (예: --key slack_channel)'));
1959
2005
  process.exit(1);
1960
2006
  }
1961
2007
  const result = await (0, kb_1.ontoAddKey)(pool, {
@@ -1974,13 +2020,13 @@ kbCmd
1974
2020
  process.exit(1);
1975
2021
  }
1976
2022
  }
1977
- else if (action === "remove-key") {
2023
+ else if (action === 'remove-key') {
1978
2024
  if (!options.type) {
1979
- console.log(chalk_1.default.red("--type 옵션이 필요합니다."));
2025
+ console.log(chalk_1.default.red('--type 옵션이 필요합니다.'));
1980
2026
  process.exit(1);
1981
2027
  }
1982
2028
  if (!options.key) {
1983
- console.log(chalk_1.default.red("--key 옵션이 필요합니다."));
2029
+ console.log(chalk_1.default.red('--key 옵션이 필요합니다.'));
1984
2030
  process.exit(1);
1985
2031
  }
1986
2032
  const result = await (0, kb_1.ontoRemoveKey)(pool, options.type, options.key);
@@ -1992,9 +2038,9 @@ kbCmd
1992
2038
  process.exit(1);
1993
2039
  }
1994
2040
  }
1995
- else if (action === "unregister") {
2041
+ else if (action === 'unregister') {
1996
2042
  if (!options.domain) {
1997
- console.log(chalk_1.default.red("--domain 옵션이 필요합니다."));
2043
+ console.log(chalk_1.default.red('--domain 옵션이 필요합니다.'));
1998
2044
  process.exit(1);
1999
2045
  }
2000
2046
  // 도메인 정보 조회
@@ -2004,12 +2050,14 @@ kbCmd
2004
2050
  process.exit(1);
2005
2051
  }
2006
2052
  // KB 엔트리 수 확인
2007
- const countRes = await pool.query("SELECT COUNT(*)::int AS cnt FROM semo.knowledge_base WHERE domain = $1", [options.domain]);
2053
+ const countRes = await pool.query('SELECT COUNT(*)::int AS cnt FROM semo.knowledge_base WHERE domain = $1', [options.domain]);
2008
2054
  const kbCount = countRes.rows[0].cnt;
2009
2055
  // 확인 프롬프트
2010
2056
  if (!options.yes) {
2011
- const typeStr = domainInfo.entity_type ? ` [${domainInfo.entity_type}]` : "";
2012
- const svcStr = domainInfo.service && domainInfo.service !== "_global" ? ` (${domainInfo.service})` : "";
2057
+ const typeStr = domainInfo.entity_type ? ` [${domainInfo.entity_type}]` : '';
2058
+ const svcStr = domainInfo.service && domainInfo.service !== '_global'
2059
+ ? ` (${domainInfo.service})`
2060
+ : '';
2013
2061
  if (kbCount > 0 && options.force) {
2014
2062
  console.log(chalk_1.default.yellow(`\n⚠️ 도메인 '${options.domain}'에 KB 항목 ${kbCount}건이 남아있습니다.`));
2015
2063
  console.log(chalk_1.default.yellow(` --force 옵션으로 모두 삭제됩니다.\n`));
@@ -2018,20 +2066,20 @@ kbCmd
2018
2066
  console.log(chalk_1.default.cyan(`\n📐 삭제 대상 도메인: ${options.domain}${typeStr}${svcStr}`));
2019
2067
  console.log(chalk_1.default.gray(` KB 항목: ${kbCount}건\n`));
2020
2068
  }
2021
- const { createInterface } = await import("readline");
2069
+ const { createInterface } = await import('readline');
2022
2070
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2023
2071
  const answer = await new Promise((resolve) => {
2024
- rl.question(chalk_1.default.yellow(" 정말 삭제하시겠습니까? (y/N): "), resolve);
2072
+ rl.question(chalk_1.default.yellow(' 정말 삭제하시겠습니까? (y/N): '), resolve);
2025
2073
  });
2026
2074
  rl.close();
2027
- if (answer.toLowerCase() !== "y") {
2028
- console.log(chalk_1.default.gray(" 취소됨.\n"));
2075
+ if (answer.toLowerCase() !== 'y') {
2076
+ console.log(chalk_1.default.gray(' 취소됨.\n'));
2029
2077
  await (0, database_1.closeConnection)();
2030
2078
  return;
2031
2079
  }
2032
2080
  }
2033
2081
  const result = await (0, kb_1.ontoUnregister)(pool, options.domain, !!options.force);
2034
- if (options.format === "json") {
2082
+ if (options.format === 'json') {
2035
2083
  console.log(JSON.stringify(result, null, 2));
2036
2084
  }
2037
2085
  else {
@@ -2061,32 +2109,32 @@ kbCmd
2061
2109
  }
2062
2110
  });
2063
2111
  // === Ontology 관리 ===
2064
- const ontoCmd = program
2065
- .command("onto")
2066
- .description("온톨로지(Ontology) 관리 — 도메인 스키마 정의");
2112
+ const ontoCmd = program.command('onto').description('온톨로지(Ontology) 관리 — 도메인 스키마 정의');
2067
2113
  ontoCmd
2068
- .command("list")
2069
- .description("정의된 온톨로지 도메인 목록")
2070
- .option("--service <name>", "서비스별 필터")
2071
- .option("--format <type>", "출력 형식 (table|json)", "table")
2114
+ .command('list')
2115
+ .description('정의된 온톨로지 도메인 목록')
2116
+ .option('--service <name>', '서비스별 필터')
2117
+ .option('--format <type>', '출력 형식 (table|json)', 'table')
2072
2118
  .action(async (options) => {
2073
2119
  try {
2074
2120
  const pool = (0, database_1.getPool)();
2075
2121
  let domains = await (0, kb_1.ontoList)(pool);
2076
2122
  if (options.service) {
2077
- domains = domains.filter(d => d.service === options.service || d.domain === options.service || d.domain.startsWith(`${options.service}.`));
2123
+ domains = domains.filter((d) => d.service === options.service ||
2124
+ d.domain === options.service ||
2125
+ d.domain.startsWith(`${options.service}.`));
2078
2126
  }
2079
- if (options.format === "json") {
2127
+ if (options.format === 'json') {
2080
2128
  console.log(JSON.stringify(domains, null, 2));
2081
2129
  }
2082
2130
  else {
2083
- console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 도메인\n"));
2131
+ console.log(chalk_1.default.cyan.bold('\n📐 온톨로지 도메인\n'));
2084
2132
  if (domains.length === 0) {
2085
- console.log(chalk_1.default.yellow(" 온톨로지가 정의되지 않았습니다."));
2133
+ console.log(chalk_1.default.yellow(' 온톨로지가 정의되지 않았습니다.'));
2086
2134
  }
2087
2135
  else {
2088
2136
  // Group by service (_global treated as Global)
2089
- const global = domains.filter(d => !d.service || d.service === '_global');
2137
+ const global = domains.filter((d) => !d.service || d.service === '_global');
2090
2138
  const byService = {};
2091
2139
  for (const d of domains) {
2092
2140
  if (d.service && d.service !== '_global') {
@@ -2096,9 +2144,9 @@ ontoCmd
2096
2144
  }
2097
2145
  }
2098
2146
  if (global.length > 0) {
2099
- console.log(chalk_1.default.white.bold(" Global"));
2147
+ console.log(chalk_1.default.white.bold(' Global'));
2100
2148
  for (const d of global) {
2101
- const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
2149
+ const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : '';
2102
2150
  console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + chalk_1.default.gray(` (v${d.version})`));
2103
2151
  if (d.description)
2104
2152
  console.log(chalk_1.default.gray(` ${d.description}`));
@@ -2107,7 +2155,7 @@ ontoCmd
2107
2155
  for (const [svc, svcDomains] of Object.entries(byService)) {
2108
2156
  console.log(chalk_1.default.white.bold(`\n Service: ${svc}`));
2109
2157
  for (const d of svcDomains) {
2110
- const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
2158
+ const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : '';
2111
2159
  console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + chalk_1.default.gray(` (v${d.version})`));
2112
2160
  if (d.description)
2113
2161
  console.log(chalk_1.default.gray(` ${d.description}`));
@@ -2125,20 +2173,20 @@ ontoCmd
2125
2173
  }
2126
2174
  });
2127
2175
  ontoCmd
2128
- .command("types")
2129
- .description("온톨로지 타입 목록 (구조적 템플릿)")
2130
- .option("--format <type>", "출력 형식 (table|json)", "table")
2176
+ .command('types')
2177
+ .description('온톨로지 타입 목록 (구조적 템플릿)')
2178
+ .option('--format <type>', '출력 형식 (table|json)', 'table')
2131
2179
  .action(async (options) => {
2132
2180
  try {
2133
2181
  const pool = (0, database_1.getPool)();
2134
2182
  const types = await (0, kb_1.ontoListTypes)(pool);
2135
- if (options.format === "json") {
2183
+ if (options.format === 'json') {
2136
2184
  console.log(JSON.stringify(types, null, 2));
2137
2185
  }
2138
2186
  else {
2139
- console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 타입\n"));
2187
+ console.log(chalk_1.default.cyan.bold('\n📐 온톨로지 타입\n'));
2140
2188
  if (types.length === 0) {
2141
- console.log(chalk_1.default.yellow(" 타입이 정의되지 않았습니다. (016 마이그레이션 실행 필요)"));
2189
+ console.log(chalk_1.default.yellow(' 타입이 정의되지 않았습니다. (016 마이그레이션 실행 필요)'));
2142
2190
  }
2143
2191
  else {
2144
2192
  for (const t of types) {
@@ -2158,8 +2206,8 @@ ontoCmd
2158
2206
  }
2159
2207
  });
2160
2208
  ontoCmd
2161
- .command("show <domain>")
2162
- .description("특정 도메인 온톨로지 상세")
2209
+ .command('show <domain>')
2210
+ .description('특정 도메인 온톨로지 상세')
2163
2211
  .action(async (domain) => {
2164
2212
  try {
2165
2213
  const pool = (0, database_1.getPool)();
@@ -2173,7 +2221,10 @@ ontoCmd
2173
2221
  console.log(chalk_1.default.white(` ${onto.description}`));
2174
2222
  console.log(chalk_1.default.gray(` 버전: ${onto.version}`));
2175
2223
  console.log(chalk_1.default.gray(` 스키마:\n`));
2176
- console.log(chalk_1.default.white(JSON.stringify(onto.schema, null, 2).split("\n").map(l => " " + l).join("\n")));
2224
+ console.log(chalk_1.default.white(JSON.stringify(onto.schema, null, 2)
2225
+ .split('\n')
2226
+ .map((l) => ' ' + l)
2227
+ .join('\n')));
2177
2228
  console.log();
2178
2229
  await (0, database_1.closeConnection)();
2179
2230
  }
@@ -2184,25 +2235,25 @@ ontoCmd
2184
2235
  }
2185
2236
  });
2186
2237
  ontoCmd
2187
- .command("validate [domain]")
2188
- .description("KB 항목이 온톨로지 스키마와 일치하는지 검증")
2189
- .option("--all", "모든 도메인 검증")
2238
+ .command('validate [domain]')
2239
+ .description('KB 항목이 온톨로지 스키마와 일치하는지 검증')
2240
+ .option('--all', '모든 도메인 검증')
2190
2241
  .action(async (domain, options) => {
2191
2242
  try {
2192
2243
  const pool = (0, database_1.getPool)();
2193
2244
  const domainsToCheck = [];
2194
2245
  if (options.all) {
2195
2246
  const allDomains = await (0, kb_1.ontoList)(pool);
2196
- domainsToCheck.push(...allDomains.map(d => d.domain));
2247
+ domainsToCheck.push(...allDomains.map((d) => d.domain));
2197
2248
  }
2198
2249
  else if (domain) {
2199
2250
  domainsToCheck.push(domain);
2200
2251
  }
2201
2252
  else {
2202
- console.log(chalk_1.default.red("도메인을 지정하거나 --all 옵션을 사용하세요."));
2253
+ console.log(chalk_1.default.red('도메인을 지정하거나 --all 옵션을 사용하세요.'));
2203
2254
  process.exit(1);
2204
2255
  }
2205
- console.log(chalk_1.default.cyan.bold("\n🔍 온톨로지 검증\n"));
2256
+ console.log(chalk_1.default.cyan.bold('\n🔍 온톨로지 검증\n'));
2206
2257
  let totalValid = 0;
2207
2258
  let totalInvalid = 0;
2208
2259
  for (const d of domainsToCheck) {
@@ -2216,7 +2267,7 @@ ontoCmd
2216
2267
  console.log(chalk_1.default.yellow(` ⚠️ ${d}: ${result.valid}건 유효, ${result.invalid.length}건 오류`));
2217
2268
  for (const inv of result.invalid) {
2218
2269
  console.log(chalk_1.default.red(` ${inv.key}:`));
2219
- inv.errors.forEach(e => console.log(chalk_1.default.gray(` - ${e}`)));
2270
+ inv.errors.forEach((e) => console.log(chalk_1.default.gray(` - ${e}`)));
2220
2271
  }
2221
2272
  }
2222
2273
  }
@@ -2230,18 +2281,20 @@ ontoCmd
2230
2281
  }
2231
2282
  });
2232
2283
  ontoCmd
2233
- .command("register <domain>")
2234
- .description("새 온톨로지 도메인 등록")
2235
- .requiredOption("--type <type>", "엔티티 타입 (예: service, team, person, bot)")
2236
- .option("--description <text>", "도메인 설명")
2237
- .option("--service <name>", "서비스 그룹")
2238
- .option("--tags <tags>", "태그 (쉼표 구분)")
2239
- .option("--no-init", "필수 KB entry 자동 생성 건너뛰기")
2240
- .option("--format <type>", "출력 형식 (json|table)", "table")
2284
+ .command('register <domain>')
2285
+ .description('새 온톨로지 도메인 등록')
2286
+ .requiredOption('--type <type>', '엔티티 타입 (예: service, team, person, bot)')
2287
+ .option('--description <text>', '도메인 설명')
2288
+ .option('--service <name>', '서비스 그룹')
2289
+ .option('--tags <tags>', '태그 (쉼표 구분)')
2290
+ .option('--no-init', '필수 KB entry 자동 생성 건너뛰기')
2291
+ .option('--format <type>', '출력 형식 (json|table)', 'table')
2241
2292
  .action(async (domain, options) => {
2242
2293
  try {
2243
2294
  const pool = (0, database_1.getPool)();
2244
- const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : undefined;
2295
+ const tags = options.tags
2296
+ ? options.tags.split(',').map((t) => t.trim())
2297
+ : undefined;
2245
2298
  const result = await (0, kb_1.ontoRegister)(pool, {
2246
2299
  domain,
2247
2300
  entity_type: options.type,
@@ -2250,7 +2303,7 @@ ontoCmd
2250
2303
  tags,
2251
2304
  init_required: options.init !== false,
2252
2305
  });
2253
- if (options.format === "json") {
2306
+ if (options.format === 'json') {
2254
2307
  console.log(JSON.stringify(result, null, 2));
2255
2308
  }
2256
2309
  else {
@@ -2278,11 +2331,11 @@ ontoCmd
2278
2331
  }
2279
2332
  });
2280
2333
  ontoCmd
2281
- .command("unregister <domain>")
2282
- .description("온톨로지 도메인 삭제 (KB 데이터 포함)")
2283
- .option("--force", "잔존 KB 항목이 있어도 모두 삭제 후 도메인 제거")
2284
- .option("--yes", "확인 프롬프트 건너뛰기")
2285
- .option("--format <type>", "출력 형식 (json|table)", "table")
2334
+ .command('unregister <domain>')
2335
+ .description('온톨로지 도메인 삭제 (KB 데이터 포함)')
2336
+ .option('--force', '잔존 KB 항목이 있어도 모두 삭제 후 도메인 제거')
2337
+ .option('--yes', '확인 프롬프트 건너뛰기')
2338
+ .option('--format <type>', '출력 형식 (json|table)', 'table')
2286
2339
  .action(async (domain, options) => {
2287
2340
  try {
2288
2341
  const pool = (0, database_1.getPool)();
@@ -2294,12 +2347,12 @@ ontoCmd
2294
2347
  process.exit(1);
2295
2348
  }
2296
2349
  // KB 엔트리 수 확인
2297
- const countRes = await pool.query("SELECT COUNT(*)::int AS cnt FROM semo.knowledge_base WHERE domain = $1", [domain]);
2350
+ const countRes = await pool.query('SELECT COUNT(*)::int AS cnt FROM semo.knowledge_base WHERE domain = $1', [domain]);
2298
2351
  const kbCount = countRes.rows[0].cnt;
2299
2352
  // 확인 프롬프트
2300
2353
  if (!options.yes) {
2301
- const typeStr = domainInfo.entity_type ? ` [${domainInfo.entity_type}]` : "";
2302
- const svcStr = domainInfo.service && domainInfo.service !== "_global" ? ` (${domainInfo.service})` : "";
2354
+ const typeStr = domainInfo.entity_type ? ` [${domainInfo.entity_type}]` : '';
2355
+ const svcStr = domainInfo.service && domainInfo.service !== '_global' ? ` (${domainInfo.service})` : '';
2303
2356
  if (kbCount > 0 && options.force) {
2304
2357
  console.log(chalk_1.default.yellow(`\n⚠️ 도메인 '${domain}'에 KB 항목 ${kbCount}건이 남아있습니다.`));
2305
2358
  console.log(chalk_1.default.yellow(` --force 옵션으로 모두 삭제됩니다.\n`));
@@ -2308,20 +2361,20 @@ ontoCmd
2308
2361
  console.log(chalk_1.default.cyan(`\n📐 삭제 대상 도메인: ${domain}${typeStr}${svcStr}`));
2309
2362
  console.log(chalk_1.default.gray(` KB 항목: ${kbCount}건\n`));
2310
2363
  }
2311
- const { createInterface } = await import("readline");
2364
+ const { createInterface } = await import('readline');
2312
2365
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2313
2366
  const answer = await new Promise((resolve) => {
2314
- rl.question(chalk_1.default.yellow(" 정말 삭제하시겠습니까? (y/N): "), resolve);
2367
+ rl.question(chalk_1.default.yellow(' 정말 삭제하시겠습니까? (y/N): '), resolve);
2315
2368
  });
2316
2369
  rl.close();
2317
- if (answer.toLowerCase() !== "y") {
2318
- console.log(chalk_1.default.gray(" 취소됨.\n"));
2370
+ if (answer.toLowerCase() !== 'y') {
2371
+ console.log(chalk_1.default.gray(' 취소됨.\n'));
2319
2372
  await (0, database_1.closeConnection)();
2320
2373
  return;
2321
2374
  }
2322
2375
  }
2323
2376
  const result = await (0, kb_1.ontoUnregister)(pool, domain, !!options.force);
2324
- if (options.format === "json") {
2377
+ if (options.format === 'json') {
2325
2378
  console.log(JSON.stringify(result, null, 2));
2326
2379
  }
2327
2380
  else {
@@ -2354,6 +2407,9 @@ ontoCmd
2354
2407
  (0, memory_1.registerMemoryCommands)(program);
2355
2408
  (0, test_1.registerTestCommands)(program);
2356
2409
  (0, commitments_1.registerCommitmentsCommands)(program);
2410
+ (0, service_1.registerServiceCommands)(program);
2411
+ (0, harness_1.registerHarnessCommands)(program);
2412
+ (0, incubator_1.registerIncubatorCommands)(program);
2357
2413
  // === semo skills — DB 시딩 ===
2358
2414
  /**
2359
2415
  * SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
@@ -2364,14 +2420,14 @@ function parseSkillFrontmatter(content) {
2364
2420
  return null;
2365
2421
  const fm = fmMatch[1];
2366
2422
  const nameMatch = fm.match(/^name:\s*(.+)$/m);
2367
- const name = nameMatch ? nameMatch[1].trim() : "";
2423
+ const name = nameMatch ? nameMatch[1].trim() : '';
2368
2424
  if (!name)
2369
2425
  return null;
2370
2426
  // multi-line description (| block scalar)
2371
- let description = "";
2427
+ let description = '';
2372
2428
  const descBlockMatch = fm.match(/^description:\s*\|\n([\s\S]*?)(?=^[a-z]|\n---)/m);
2373
2429
  if (descBlockMatch) {
2374
- description = descBlockMatch[1].replace(/^ /gm, "").trim();
2430
+ description = descBlockMatch[1].replace(/^ /gm, '').trim();
2375
2431
  }
2376
2432
  else {
2377
2433
  const descInlineMatch = fm.match(/^description:\s*(.+)$/m);
@@ -2380,9 +2436,9 @@ function parseSkillFrontmatter(content) {
2380
2436
  }
2381
2437
  // tools 배열
2382
2438
  const toolsMatch = fm.match(/^tools:\s*\[(.+)\]$/m);
2383
- const tools = toolsMatch ? toolsMatch[1].split(",").map((t) => t.trim()) : [];
2439
+ const tools = toolsMatch ? toolsMatch[1].split(',').map((t) => t.trim()) : [];
2384
2440
  // category
2385
- const category = "core";
2441
+ const category = 'core';
2386
2442
  return { name, description, category, tools };
2387
2443
  }
2388
2444
  // semo skills seed — 제거됨 (중앙 DB 단일 SoT)
@@ -2391,7 +2447,7 @@ function parseSkillFrontmatter(content) {
2391
2447
  async function main() {
2392
2448
  const args = process.argv.slice(2);
2393
2449
  // semo -v 또는 semo --version-info 처리
2394
- if (args.length === 1 && (args[0] === "-v" || args[0] === "--version-info")) {
2450
+ if (args.length === 1 && (args[0] === '-v' || args[0] === '--version-info')) {
2395
2451
  await showVersionInfo();
2396
2452
  process.exit(0);
2397
2453
  }