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