@team-semicolon/semo-cli 4.13.0 → 4.15.1

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