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