@team-semicolon/semo-cli 3.14.0 → 4.0.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/bots.d.ts +11 -0
- package/dist/commands/bots.js +348 -0
- package/dist/commands/context.d.ts +8 -0
- package/dist/commands/context.js +302 -0
- package/dist/commands/get.d.ts +12 -0
- package/dist/commands/get.js +433 -0
- package/dist/database.d.ts +2 -0
- package/dist/database.js +23 -10
- package/dist/index.d.ts +6 -9
- package/dist/index.js +750 -1298
- package/dist/kb.d.ts +134 -0
- package/dist/kb.js +627 -0
- package/package.json +4 -4
- package/dist/supabase.d.ts +0 -97
- package/dist/supabase.js +0 -602
package/dist/index.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
|
-
* SEMO CLI
|
|
4
|
+
* SEMO CLI v4.0
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Core DB 기반 컨텍스트 동기화 시스템
|
|
7
7
|
*
|
|
8
8
|
* 사용법:
|
|
9
|
-
* npx @team-semicolon/semo-cli init # 기본 설치
|
|
10
|
-
* npx @team-semicolon/semo-cli
|
|
11
|
-
* npx @team-semicolon/semo-cli
|
|
12
|
-
*
|
|
13
|
-
* 구조:
|
|
14
|
-
* - Standard: semo-core + semo-skills (필수)
|
|
15
|
-
* - Extensions: packages/next, packages/backend 등 (선택)
|
|
9
|
+
* npx @team-semicolon/semo-cli init # 기본 설치 (훅 등록 포함)
|
|
10
|
+
* npx @team-semicolon/semo-cli context sync # DB → .claude/memory/
|
|
11
|
+
* npx @team-semicolon/semo-cli bots status # 봇 상태 조회
|
|
12
|
+
* npx @team-semicolon/semo-cli get kb # KB 실시간 쿼리
|
|
16
13
|
*/
|
|
17
14
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
15
|
if (k2 === undefined) k2 = k;
|
|
@@ -60,6 +57,9 @@ const fs = __importStar(require("fs"));
|
|
|
60
57
|
const path = __importStar(require("path"));
|
|
61
58
|
const os = __importStar(require("os"));
|
|
62
59
|
const database_1 = require("./database");
|
|
60
|
+
const context_1 = require("./commands/context");
|
|
61
|
+
const bots_1 = require("./commands/bots");
|
|
62
|
+
const get_1 = require("./commands/get");
|
|
63
63
|
const PACKAGE_NAME = "@team-semicolon/semo-cli";
|
|
64
64
|
// package.json에서 버전 동적 로드
|
|
65
65
|
function getCliVersion() {
|
|
@@ -236,115 +236,6 @@ async function showVersionComparison(cwd) {
|
|
|
236
236
|
level: 0,
|
|
237
237
|
});
|
|
238
238
|
}
|
|
239
|
-
// 그룹 패키지 (eng, biz, ops) 및 하위 Extension - semo-system 내부
|
|
240
|
-
// 그룹별로 묶어서 계층 구조로 출력
|
|
241
|
-
if (hasSemoSystem) {
|
|
242
|
-
for (const group of PACKAGE_GROUPS) {
|
|
243
|
-
const groupVersionPath = path.join(semoSystemDir, group, "VERSION");
|
|
244
|
-
const hasGroupVersion = fs.existsSync(groupVersionPath);
|
|
245
|
-
// 해당 그룹의 하위 패키지 찾기
|
|
246
|
-
const groupExtensions = Object.keys(EXTENSION_PACKAGES).filter(key => key.startsWith(`${group}/`));
|
|
247
|
-
const installedGroupExtensions = groupExtensions.filter(key => fs.existsSync(path.join(semoSystemDir, key, "VERSION")));
|
|
248
|
-
// 그룹 버전이 있거나 하위 패키지가 설치된 경우에만 표시
|
|
249
|
-
if (hasGroupVersion || installedGroupExtensions.length > 0) {
|
|
250
|
-
// 그룹 패키지 버전 추가
|
|
251
|
-
if (hasGroupVersion) {
|
|
252
|
-
const localGroup = fs.readFileSync(groupVersionPath, "utf-8").trim();
|
|
253
|
-
const remoteGroup = await getRemotePackageVersion(group);
|
|
254
|
-
versionInfos.push({
|
|
255
|
-
name: group,
|
|
256
|
-
local: localGroup,
|
|
257
|
-
remote: remoteGroup,
|
|
258
|
-
needsUpdate: remoteGroup ? isVersionLower(localGroup, remoteGroup) : false,
|
|
259
|
-
level: 1,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
// 하위 Extension 패키지들 추가
|
|
263
|
-
for (const key of installedGroupExtensions) {
|
|
264
|
-
const extVersionPath = path.join(semoSystemDir, key, "VERSION");
|
|
265
|
-
const localExt = fs.readFileSync(extVersionPath, "utf-8").trim();
|
|
266
|
-
const remoteExt = await getRemotePackageVersion(key);
|
|
267
|
-
versionInfos.push({
|
|
268
|
-
name: key,
|
|
269
|
-
local: localExt,
|
|
270
|
-
remote: remoteExt,
|
|
271
|
-
needsUpdate: remoteExt ? isVersionLower(localExt, remoteExt) : false,
|
|
272
|
-
level: 2,
|
|
273
|
-
group: group,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
// 그룹에 속하지 않는 Extension (meta 등)
|
|
279
|
-
const nonGroupExtensions = Object.keys(EXTENSION_PACKAGES).filter(key => !PACKAGE_GROUPS.some(g => key.startsWith(`${g}/`)));
|
|
280
|
-
for (const key of nonGroupExtensions) {
|
|
281
|
-
const extVersionPath = path.join(semoSystemDir, key, "VERSION");
|
|
282
|
-
if (fs.existsSync(extVersionPath)) {
|
|
283
|
-
const localExt = fs.readFileSync(extVersionPath, "utf-8").trim();
|
|
284
|
-
const remoteExt = await getRemotePackageVersion(key);
|
|
285
|
-
versionInfos.push({
|
|
286
|
-
name: key,
|
|
287
|
-
local: localExt,
|
|
288
|
-
remote: remoteExt,
|
|
289
|
-
needsUpdate: remoteExt ? isVersionLower(localExt, remoteExt) : false,
|
|
290
|
-
level: 1,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// packages/ 디렉토리의 설치된 패키지들 (로컬 버전만 표시) - 개발 환경용
|
|
296
|
-
const packagesDir = path.join(cwd, "packages");
|
|
297
|
-
if (fs.existsSync(packagesDir)) {
|
|
298
|
-
// 그룹별 패키지 매핑
|
|
299
|
-
const packageGroups = {
|
|
300
|
-
"packages/core": { level: 0, packages: [{ name: "packages/core", path: "core" }] },
|
|
301
|
-
"packages/meta": { level: 0, packages: [{ name: "packages/meta", path: "meta" }] },
|
|
302
|
-
"packages/eng": {
|
|
303
|
-
level: 1,
|
|
304
|
-
packages: [
|
|
305
|
-
{ name: "packages/eng/nextjs", path: "eng/nextjs" },
|
|
306
|
-
{ name: "packages/eng/spring", path: "eng/spring" },
|
|
307
|
-
{ name: "packages/eng/ms", path: "eng/ms" },
|
|
308
|
-
{ name: "packages/eng/infra", path: "eng/infra" },
|
|
309
|
-
],
|
|
310
|
-
},
|
|
311
|
-
"packages/biz": {
|
|
312
|
-
level: 1,
|
|
313
|
-
packages: [
|
|
314
|
-
{ name: "packages/biz/discovery", path: "biz/discovery" },
|
|
315
|
-
{ name: "packages/biz/management", path: "biz/management" },
|
|
316
|
-
{ name: "packages/biz/design", path: "biz/design" },
|
|
317
|
-
{ name: "packages/biz/poc", path: "biz/poc" },
|
|
318
|
-
],
|
|
319
|
-
},
|
|
320
|
-
"packages/ops": {
|
|
321
|
-
level: 1,
|
|
322
|
-
packages: [
|
|
323
|
-
{ name: "packages/ops/qa", path: "ops/qa" },
|
|
324
|
-
{ name: "packages/ops/monitor", path: "ops/monitor" },
|
|
325
|
-
{ name: "packages/ops/improve", path: "ops/improve" },
|
|
326
|
-
],
|
|
327
|
-
},
|
|
328
|
-
};
|
|
329
|
-
for (const [groupKey, groupData] of Object.entries(packageGroups)) {
|
|
330
|
-
for (const pkg of groupData.packages) {
|
|
331
|
-
const pkgVersionPath = path.join(packagesDir, pkg.path, "VERSION");
|
|
332
|
-
if (fs.existsSync(pkgVersionPath)) {
|
|
333
|
-
const localPkg = fs.readFileSync(pkgVersionPath, "utf-8").trim();
|
|
334
|
-
const remotePkg = await getRemotePackageVersion(`packages/${pkg.path}`);
|
|
335
|
-
const isSubPackage = pkg.path.includes("/");
|
|
336
|
-
versionInfos.push({
|
|
337
|
-
name: pkg.name,
|
|
338
|
-
local: localPkg,
|
|
339
|
-
remote: remotePkg,
|
|
340
|
-
needsUpdate: remotePkg ? isVersionLower(localPkg, remotePkg) : false,
|
|
341
|
-
level: isSubPackage ? 2 : groupData.level,
|
|
342
|
-
group: isSubPackage ? pkg.path.split("/")[0] : undefined,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
239
|
spinner.stop();
|
|
349
240
|
// 결과 출력
|
|
350
241
|
const needsUpdateCount = versionInfos.filter(v => v.needsUpdate).length;
|
|
@@ -505,23 +396,6 @@ function detectLegacyEnvironment(cwd) {
|
|
|
505
396
|
legacyPaths.push(dir);
|
|
506
397
|
}
|
|
507
398
|
}
|
|
508
|
-
// semo-system/ 내부의 레거시 Extension 구조 확인
|
|
509
|
-
// 구버전: semo-system/biz/, semo-system/eng/, semo-system/ops/ (그룹 디렉토리)
|
|
510
|
-
// 신버전: semo-system/biz/design/, semo-system/eng/nextjs/ 등 (개별 패키지)
|
|
511
|
-
const semoSystemDir = path.join(cwd, "semo-system");
|
|
512
|
-
if (fs.existsSync(semoSystemDir)) {
|
|
513
|
-
const legacyExtGroups = ["biz", "eng", "ops"];
|
|
514
|
-
for (const group of legacyExtGroups) {
|
|
515
|
-
const groupDir = path.join(semoSystemDir, group);
|
|
516
|
-
if (fs.existsSync(groupDir) && fs.statSync(groupDir).isDirectory()) {
|
|
517
|
-
// VERSION 파일이 그룹 디렉토리에 직접 있으면 레거시 구조
|
|
518
|
-
const groupVersionFile = path.join(groupDir, "VERSION");
|
|
519
|
-
if (fs.existsSync(groupVersionFile)) {
|
|
520
|
-
legacyPaths.push(`semo-system/${group} (레거시 그룹 구조)`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
399
|
// .claude/ 내부의 레거시 구조 확인
|
|
526
400
|
const claudeDir = path.join(cwd, ".claude");
|
|
527
401
|
if (fs.existsSync(claudeDir)) {
|
|
@@ -604,22 +478,9 @@ async function migrateLegacyEnvironment(cwd) {
|
|
|
604
478
|
}
|
|
605
479
|
}
|
|
606
480
|
}
|
|
607
|
-
// 3. semo-system
|
|
608
|
-
const semoSystemDir = path.join(cwd, "semo-system");
|
|
609
|
-
if (fs.existsSync(semoSystemDir)) {
|
|
610
|
-
const legacyExtGroups = ["biz", "eng", "ops"];
|
|
611
|
-
for (const group of legacyExtGroups) {
|
|
612
|
-
const groupDir = path.join(semoSystemDir, group);
|
|
613
|
-
const groupVersionFile = path.join(groupDir, "VERSION");
|
|
614
|
-
// VERSION 파일이 그룹 디렉토리에 직접 있으면 레거시 구조이므로 삭제
|
|
615
|
-
if (fs.existsSync(groupVersionFile)) {
|
|
616
|
-
removeRecursive(groupDir);
|
|
617
|
-
console.log(chalk_1.default.gray(` ✓ semo-system/${group}/ 삭제됨 (레거시 그룹 구조)`));
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
// 4. 기존 semo-system이 완전히 레거시인 경우에만 삭제
|
|
481
|
+
// 3. 기존 semo-system이 완전히 레거시인 경우에만 삭제
|
|
622
482
|
// (Standard 패키지가 없는 경우)
|
|
483
|
+
const semoSystemDir = path.join(cwd, "semo-system");
|
|
623
484
|
if (fs.existsSync(semoSystemDir)) {
|
|
624
485
|
const hasStandard = fs.existsSync(path.join(semoSystemDir, "semo-core"));
|
|
625
486
|
if (!hasStandard) {
|
|
@@ -674,130 +535,6 @@ function copyRecursive(src, dest) {
|
|
|
674
535
|
}
|
|
675
536
|
}
|
|
676
537
|
const SEMO_REPO = "https://github.com/semicolon-devteam/semo.git";
|
|
677
|
-
// ============================================================
|
|
678
|
-
// 패키지 관리 (v3.14.0 - 폴백 데이터 사용)
|
|
679
|
-
// ============================================================
|
|
680
|
-
// v3.14.0: Extensions는 아직 git 기반이므로 폴백 데이터 직접 사용
|
|
681
|
-
// 향후 Extensions도 DB 기반으로 전환 예정
|
|
682
|
-
// 캐시된 패키지 데이터
|
|
683
|
-
let cachedExtensionPackages = null;
|
|
684
|
-
let cachedShortnameMappings = null;
|
|
685
|
-
// 패키지 데이터 초기화 (폴백 데이터 사용)
|
|
686
|
-
async function initPackageData() {
|
|
687
|
-
if (cachedExtensionPackages && cachedShortnameMappings)
|
|
688
|
-
return;
|
|
689
|
-
// v3.14.0: Extensions는 아직 git 기반이므로 폴백 데이터 사용
|
|
690
|
-
cachedExtensionPackages = EXTENSION_PACKAGES_FALLBACK;
|
|
691
|
-
cachedShortnameMappings = SHORTNAME_MAPPING_FALLBACK;
|
|
692
|
-
}
|
|
693
|
-
// EXTENSION_PACKAGES 동기 접근용 (초기화 후 사용)
|
|
694
|
-
function getExtensionPackagesSync() {
|
|
695
|
-
return cachedExtensionPackages || EXTENSION_PACKAGES_FALLBACK;
|
|
696
|
-
}
|
|
697
|
-
// SHORTNAME_MAPPING 동기 접근용
|
|
698
|
-
function getShortnameMappingSync() {
|
|
699
|
-
return cachedShortnameMappings || SHORTNAME_MAPPING_FALLBACK;
|
|
700
|
-
}
|
|
701
|
-
// 폴백용 하드코딩 데이터 (DB 연결 실패 시 사용)
|
|
702
|
-
const EXTENSION_PACKAGES_FALLBACK = {
|
|
703
|
-
// Business Layer
|
|
704
|
-
"biz/discovery": { name: "Discovery", desc: "아이템 발굴, 시장 조사, Epic/Task", layer: "biz", detect: [] },
|
|
705
|
-
"biz/design": { name: "Design", desc: "컨셉 설계, 목업, UX", layer: "biz", detect: [] },
|
|
706
|
-
"biz/management": { name: "Management", desc: "일정/인력/스프린트 관리", layer: "biz", detect: [] },
|
|
707
|
-
"biz/poc": { name: "PoC", desc: "빠른 PoC, 패스트트랙", layer: "biz", detect: [] },
|
|
708
|
-
// Engineering Layer
|
|
709
|
-
"eng/nextjs": { name: "Next.js", desc: "Next.js 프론트엔드 개발", layer: "eng", detect: ["next.config.js", "next.config.mjs", "next.config.ts"] },
|
|
710
|
-
"eng/spring": { name: "Spring", desc: "Spring Boot 백엔드 개발", layer: "eng", detect: ["pom.xml", "build.gradle"] },
|
|
711
|
-
"eng/ms": { name: "Microservice", desc: "마이크로서비스 아키텍처", layer: "eng", detect: [] },
|
|
712
|
-
"eng/infra": { name: "Infra", desc: "인프라/배포 관리", layer: "eng", detect: ["docker-compose.yml", "Dockerfile"] },
|
|
713
|
-
// Operations Layer
|
|
714
|
-
"ops/qa": { name: "QA", desc: "테스트/품질 관리", layer: "ops", detect: [] },
|
|
715
|
-
"ops/monitor": { name: "Monitor", desc: "서비스 현황 모니터링", layer: "ops", detect: [] },
|
|
716
|
-
"ops/improve": { name: "Improve", desc: "개선 제안", layer: "ops", detect: [] },
|
|
717
|
-
// Meta
|
|
718
|
-
meta: { name: "Meta", desc: "SEMO 프레임워크 자체 개발/관리", layer: "meta", detect: ["semo-core", "semo-skills"] },
|
|
719
|
-
// System (semo-system 하위 패키지)
|
|
720
|
-
"semo-hooks": { name: "Hooks", desc: "Claude Code Hooks 기반 로깅 시스템", layer: "system", detect: [] },
|
|
721
|
-
"semo-remote": { name: "Remote", desc: "Claude Code 원격 제어 (모바일 PWA)", layer: "system", detect: [] },
|
|
722
|
-
};
|
|
723
|
-
// 단축명 → 전체 패키지 경로 매핑 (폴백)
|
|
724
|
-
const SHORTNAME_MAPPING_FALLBACK = {
|
|
725
|
-
// 하위 패키지명 단축 (discovery → biz/discovery)
|
|
726
|
-
discovery: "biz/discovery",
|
|
727
|
-
design: "biz/design",
|
|
728
|
-
management: "biz/management",
|
|
729
|
-
poc: "biz/poc",
|
|
730
|
-
nextjs: "eng/nextjs",
|
|
731
|
-
spring: "eng/spring",
|
|
732
|
-
ms: "eng/ms",
|
|
733
|
-
infra: "eng/infra",
|
|
734
|
-
qa: "ops/qa",
|
|
735
|
-
monitor: "ops/monitor",
|
|
736
|
-
improve: "ops/improve",
|
|
737
|
-
// 추가 별칭
|
|
738
|
-
next: "eng/nextjs",
|
|
739
|
-
backend: "eng/spring",
|
|
740
|
-
mvp: "biz/poc",
|
|
741
|
-
// System 패키지 단축명
|
|
742
|
-
hooks: "semo-hooks",
|
|
743
|
-
remote: "semo-remote",
|
|
744
|
-
};
|
|
745
|
-
// 호환성을 위한 상수 별칭 (기존 코드에서 사용)
|
|
746
|
-
const EXTENSION_PACKAGES = EXTENSION_PACKAGES_FALLBACK;
|
|
747
|
-
const SHORTNAME_MAPPING = SHORTNAME_MAPPING_FALLBACK;
|
|
748
|
-
// 그룹 이름 목록 (biz, eng, ops, meta, system)
|
|
749
|
-
const PACKAGE_GROUPS = ["biz", "eng", "ops", "meta", "system"];
|
|
750
|
-
// 그룹명 → 해당 그룹의 모든 패키지 반환
|
|
751
|
-
async function getPackagesByGroupAsync(group) {
|
|
752
|
-
// v3.14.0: 동기 함수와 동일하게 폴백 데이터 사용
|
|
753
|
-
return getPackagesByGroupSync(group);
|
|
754
|
-
}
|
|
755
|
-
// 그룹명 → 해당 그룹의 모든 패키지 반환 (동기, 폴백)
|
|
756
|
-
function getPackagesByGroupSync(group) {
|
|
757
|
-
const extPkgs = getExtensionPackagesSync();
|
|
758
|
-
return Object.entries(extPkgs)
|
|
759
|
-
.filter(([, pkg]) => pkg.layer === group)
|
|
760
|
-
.map(([key]) => key);
|
|
761
|
-
}
|
|
762
|
-
// 패키지 입력을 해석 (그룹, 레거시, 쉼표 구분 모두 처리)
|
|
763
|
-
function resolvePackageInput(input) {
|
|
764
|
-
// 쉼표로 구분된 여러 패키지 처리
|
|
765
|
-
const parts = input.split(",").map(p => p.trim()).filter(p => p);
|
|
766
|
-
const resolvedPackages = [];
|
|
767
|
-
let isGroup = false;
|
|
768
|
-
let groupName;
|
|
769
|
-
// DB에서 로드된 데이터 또는 폴백 사용
|
|
770
|
-
const extPkgs = getExtensionPackagesSync();
|
|
771
|
-
const shortnames = getShortnameMappingSync();
|
|
772
|
-
for (const part of parts) {
|
|
773
|
-
// 1. 그룹명인지 확인 (biz, eng, ops, meta)
|
|
774
|
-
if (PACKAGE_GROUPS.includes(part)) {
|
|
775
|
-
const groupPackages = getPackagesByGroupSync(part);
|
|
776
|
-
resolvedPackages.push(...groupPackages);
|
|
777
|
-
isGroup = true;
|
|
778
|
-
groupName = part;
|
|
779
|
-
continue;
|
|
780
|
-
}
|
|
781
|
-
// 2. 단축명 매핑 확인 (discovery → biz/discovery 등)
|
|
782
|
-
if (part in shortnames) {
|
|
783
|
-
resolvedPackages.push(shortnames[part]);
|
|
784
|
-
continue;
|
|
785
|
-
}
|
|
786
|
-
// 3. 직접 패키지명 확인
|
|
787
|
-
if (part in extPkgs) {
|
|
788
|
-
resolvedPackages.push(part);
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
// 4. 유효하지 않은 패키지명
|
|
792
|
-
// (빈 배열 대신 null을 추가하여 나중에 에러 처리)
|
|
793
|
-
}
|
|
794
|
-
// 중복 제거
|
|
795
|
-
return {
|
|
796
|
-
packages: [...new Set(resolvedPackages)],
|
|
797
|
-
isGroup,
|
|
798
|
-
groupName
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
538
|
const program = new commander_1.Command();
|
|
802
539
|
program
|
|
803
540
|
.name("semo")
|
|
@@ -856,113 +593,6 @@ async function showVersionInfo() {
|
|
|
856
593
|
level: 0,
|
|
857
594
|
});
|
|
858
595
|
}
|
|
859
|
-
// 4. 그룹 패키지 (eng, biz, ops) 및 하위 Extension - semo-system 내부
|
|
860
|
-
const semoSystemDir = path.join(cwd, "semo-system");
|
|
861
|
-
if (fs.existsSync(semoSystemDir)) {
|
|
862
|
-
for (const group of PACKAGE_GROUPS) {
|
|
863
|
-
const groupVersionPath = path.join(semoSystemDir, group, "VERSION");
|
|
864
|
-
const hasGroupVersion = fs.existsSync(groupVersionPath);
|
|
865
|
-
// 해당 그룹의 하위 패키지 찾기
|
|
866
|
-
const groupExtensions = Object.keys(EXTENSION_PACKAGES).filter(key => key.startsWith(`${group}/`));
|
|
867
|
-
const installedGroupExtensions = groupExtensions.filter(key => fs.existsSync(path.join(semoSystemDir, key, "VERSION")));
|
|
868
|
-
if (hasGroupVersion || installedGroupExtensions.length > 0) {
|
|
869
|
-
// 그룹 패키지 버전 추가
|
|
870
|
-
if (hasGroupVersion) {
|
|
871
|
-
const localGroup = fs.readFileSync(groupVersionPath, "utf-8").trim();
|
|
872
|
-
const remoteGroup = await getRemotePackageVersion(group);
|
|
873
|
-
versionInfos.push({
|
|
874
|
-
name: group,
|
|
875
|
-
local: localGroup,
|
|
876
|
-
remote: remoteGroup,
|
|
877
|
-
needsUpdate: remoteGroup ? isVersionLower(localGroup, remoteGroup) : false,
|
|
878
|
-
level: 1,
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
// 하위 Extension 패키지들 추가
|
|
882
|
-
for (const key of installedGroupExtensions) {
|
|
883
|
-
const extVersionPath = path.join(semoSystemDir, key, "VERSION");
|
|
884
|
-
const localExt = fs.readFileSync(extVersionPath, "utf-8").trim();
|
|
885
|
-
const remoteExt = await getRemotePackageVersion(key);
|
|
886
|
-
versionInfos.push({
|
|
887
|
-
name: key,
|
|
888
|
-
local: localExt,
|
|
889
|
-
remote: remoteExt,
|
|
890
|
-
needsUpdate: remoteExt ? isVersionLower(localExt, remoteExt) : false,
|
|
891
|
-
level: 2,
|
|
892
|
-
group: group,
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
// 그룹에 속하지 않는 Extension (meta 등)
|
|
898
|
-
const nonGroupExtensions = Object.keys(EXTENSION_PACKAGES).filter(key => !PACKAGE_GROUPS.some(g => key.startsWith(`${g}/`)));
|
|
899
|
-
for (const key of nonGroupExtensions) {
|
|
900
|
-
const extVersionPath = path.join(semoSystemDir, key, "VERSION");
|
|
901
|
-
if (fs.existsSync(extVersionPath)) {
|
|
902
|
-
const localExt = fs.readFileSync(extVersionPath, "utf-8").trim();
|
|
903
|
-
const remoteExt = await getRemotePackageVersion(key);
|
|
904
|
-
versionInfos.push({
|
|
905
|
-
name: key,
|
|
906
|
-
local: localExt,
|
|
907
|
-
remote: remoteExt,
|
|
908
|
-
needsUpdate: remoteExt ? isVersionLower(localExt, remoteExt) : false,
|
|
909
|
-
level: 1,
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
// 5. packages/ 디렉토리의 설치된 패키지들 - 개발 환경용
|
|
915
|
-
const packagesDir = path.join(cwd, "packages");
|
|
916
|
-
if (fs.existsSync(packagesDir)) {
|
|
917
|
-
const packageGroups = {
|
|
918
|
-
"packages/core": { level: 0, packages: [{ name: "packages/core", path: "core" }] },
|
|
919
|
-
"packages/meta": { level: 0, packages: [{ name: "packages/meta", path: "meta" }] },
|
|
920
|
-
"packages/eng": {
|
|
921
|
-
level: 1,
|
|
922
|
-
packages: [
|
|
923
|
-
{ name: "packages/eng/nextjs", path: "eng/nextjs" },
|
|
924
|
-
{ name: "packages/eng/spring", path: "eng/spring" },
|
|
925
|
-
{ name: "packages/eng/ms", path: "eng/ms" },
|
|
926
|
-
{ name: "packages/eng/infra", path: "eng/infra" },
|
|
927
|
-
],
|
|
928
|
-
},
|
|
929
|
-
"packages/biz": {
|
|
930
|
-
level: 1,
|
|
931
|
-
packages: [
|
|
932
|
-
{ name: "packages/biz/discovery", path: "biz/discovery" },
|
|
933
|
-
{ name: "packages/biz/management", path: "biz/management" },
|
|
934
|
-
{ name: "packages/biz/design", path: "biz/design" },
|
|
935
|
-
{ name: "packages/biz/poc", path: "biz/poc" },
|
|
936
|
-
],
|
|
937
|
-
},
|
|
938
|
-
"packages/ops": {
|
|
939
|
-
level: 1,
|
|
940
|
-
packages: [
|
|
941
|
-
{ name: "packages/ops/qa", path: "ops/qa" },
|
|
942
|
-
{ name: "packages/ops/monitor", path: "ops/monitor" },
|
|
943
|
-
{ name: "packages/ops/improve", path: "ops/improve" },
|
|
944
|
-
],
|
|
945
|
-
},
|
|
946
|
-
};
|
|
947
|
-
for (const [, groupData] of Object.entries(packageGroups)) {
|
|
948
|
-
for (const pkg of groupData.packages) {
|
|
949
|
-
const pkgVersionPath = path.join(packagesDir, pkg.path, "VERSION");
|
|
950
|
-
if (fs.existsSync(pkgVersionPath)) {
|
|
951
|
-
const localPkg = fs.readFileSync(pkgVersionPath, "utf-8").trim();
|
|
952
|
-
const remotePkg = await getRemotePackageVersion(`packages/${pkg.path}`);
|
|
953
|
-
const isSubPackage = pkg.path.includes("/");
|
|
954
|
-
versionInfos.push({
|
|
955
|
-
name: pkg.name,
|
|
956
|
-
local: localPkg,
|
|
957
|
-
remote: remotePkg,
|
|
958
|
-
needsUpdate: remotePkg ? isVersionLower(localPkg, remotePkg) : false,
|
|
959
|
-
level: isSubPackage ? 2 : groupData.level,
|
|
960
|
-
group: isSubPackage ? pkg.path.split("/")[0] : undefined,
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
596
|
// 결과 출력
|
|
967
597
|
const needsUpdateCount = versionInfos.filter(v => v.needsUpdate).length;
|
|
968
598
|
if (versionInfos.length === 1) {
|
|
@@ -1055,30 +685,6 @@ async function confirmOverwrite(itemName, itemPath) {
|
|
|
1055
685
|
]);
|
|
1056
686
|
return shouldOverwrite;
|
|
1057
687
|
}
|
|
1058
|
-
function detectProjectType(cwd) {
|
|
1059
|
-
const detected = [];
|
|
1060
|
-
for (const [key, pkg] of Object.entries(EXTENSION_PACKAGES)) {
|
|
1061
|
-
for (const file of pkg.detect) {
|
|
1062
|
-
if (fs.existsSync(path.join(cwd, file))) {
|
|
1063
|
-
detected.push(key);
|
|
1064
|
-
break;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
return detected;
|
|
1069
|
-
}
|
|
1070
|
-
// === 설치된 Extension 패키지 스캔 ===
|
|
1071
|
-
function getInstalledExtensions(cwd) {
|
|
1072
|
-
const semoSystemDir = path.join(cwd, "semo-system");
|
|
1073
|
-
const installed = [];
|
|
1074
|
-
for (const key of Object.keys(EXTENSION_PACKAGES)) {
|
|
1075
|
-
const pkgPath = path.join(semoSystemDir, key);
|
|
1076
|
-
if (fs.existsSync(pkgPath)) {
|
|
1077
|
-
installed.push(key);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
return installed;
|
|
1081
|
-
}
|
|
1082
688
|
function checkRequiredTools() {
|
|
1083
689
|
const tools = [
|
|
1084
690
|
{
|
|
@@ -1164,13 +770,11 @@ program
|
|
|
1164
770
|
.option("-f, --force", "기존 설정 덮어쓰기")
|
|
1165
771
|
.option("--skip-mcp", "MCP 설정 생략")
|
|
1166
772
|
.option("--no-gitignore", ".gitignore 수정 생략")
|
|
1167
|
-
.option("--with <packages>", "추가 설치할 패키지 (쉼표 구분: next,backend)")
|
|
1168
773
|
.option("--migrate", "레거시 환경 강제 마이그레이션")
|
|
774
|
+
.option("--seed-skills", "semo-system/semo-skills/ → semo.skills DB 초기 시딩")
|
|
1169
775
|
.action(async (options) => {
|
|
1170
776
|
console.log(chalk_1.default.cyan.bold("\n🚀 SEMO 설치 시작\n"));
|
|
1171
777
|
console.log(chalk_1.default.gray("Gemini 하이브리드 전략: White Box + Black Box\n"));
|
|
1172
|
-
// 0. 패키지 데이터 초기화 (DB에서 조회)
|
|
1173
|
-
await initPackageData();
|
|
1174
778
|
const cwd = process.cwd();
|
|
1175
779
|
// 0.1. 버전 비교
|
|
1176
780
|
await showVersionComparison(cwd);
|
|
@@ -1198,61 +802,37 @@ program
|
|
|
1198
802
|
spinner.fail("Git 레포지토리가 아닙니다. 'git init'을 먼저 실행하세요.");
|
|
1199
803
|
process.exit(1);
|
|
1200
804
|
}
|
|
1201
|
-
// 2.
|
|
1202
|
-
let extensionsToInstall = [];
|
|
1203
|
-
const extPkgs = getExtensionPackagesSync();
|
|
1204
|
-
const shortnames = getShortnameMappingSync();
|
|
1205
|
-
if (options.with) {
|
|
1206
|
-
// --with 옵션으로 명시적 패키지 지정 시에만 Extension 설치
|
|
1207
|
-
extensionsToInstall = options.with.split(",").map((p) => p.trim()).filter((p) => p in extPkgs || p in shortnames);
|
|
1208
|
-
// 별칭 처리
|
|
1209
|
-
extensionsToInstall = extensionsToInstall.map((p) => shortnames[p] || p);
|
|
1210
|
-
}
|
|
1211
|
-
// 프로젝트 유형 감지는 정보 제공용으로만 사용 (자동 설치 안 함)
|
|
1212
|
-
const detected = detectProjectType(cwd);
|
|
1213
|
-
if (detected.length > 0 && !options.with) {
|
|
1214
|
-
console.log(chalk_1.default.cyan("\n💡 감지된 프로젝트 유형:"));
|
|
1215
|
-
detected.forEach(pkg => {
|
|
1216
|
-
const pkgInfo = extPkgs[pkg];
|
|
1217
|
-
if (pkgInfo) {
|
|
1218
|
-
console.log(chalk_1.default.gray(` - ${pkgInfo.name}: ${pkgInfo.desc}`));
|
|
1219
|
-
}
|
|
1220
|
-
});
|
|
1221
|
-
console.log(chalk_1.default.gray(`\n 추가 패키지가 필요하면: semo add ${detected[0].split("/")[1] || detected[0]}`));
|
|
1222
|
-
}
|
|
1223
|
-
// 3. .claude 디렉토리 생성
|
|
805
|
+
// 2. .claude 디렉토리 생성
|
|
1224
806
|
const claudeDir = path.join(cwd, ".claude");
|
|
1225
807
|
if (!fs.existsSync(claudeDir)) {
|
|
1226
808
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
1227
809
|
console.log(chalk_1.default.green("\n✓ .claude/ 디렉토리 생성됨"));
|
|
1228
810
|
}
|
|
1229
|
-
//
|
|
811
|
+
// 3. Standard 설치 (semo-core + semo-skills)
|
|
1230
812
|
await setupStandard(cwd, options.force);
|
|
1231
|
-
//
|
|
1232
|
-
if (extensionsToInstall.length > 0) {
|
|
1233
|
-
await downloadExtensions(cwd, extensionsToInstall, options.force);
|
|
1234
|
-
}
|
|
1235
|
-
// 6. MCP 설정 (Extension 설정 병합 포함)
|
|
813
|
+
// 4. MCP 설정
|
|
1236
814
|
if (!options.skipMcp) {
|
|
1237
|
-
await setupMCP(cwd,
|
|
815
|
+
await setupMCP(cwd, [], options.force);
|
|
1238
816
|
}
|
|
1239
|
-
//
|
|
817
|
+
// 5. Context Mesh 초기화
|
|
1240
818
|
await setupContextMesh(cwd);
|
|
1241
|
-
//
|
|
819
|
+
// 6. .gitignore 업데이트
|
|
1242
820
|
if (options.gitignore !== false) {
|
|
1243
821
|
updateGitignore(cwd);
|
|
1244
822
|
}
|
|
1245
|
-
//
|
|
823
|
+
// 7. Hooks 설치 (대화 로깅)
|
|
1246
824
|
await setupHooks(cwd, false);
|
|
1247
|
-
//
|
|
1248
|
-
await setupClaudeMd(cwd,
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
await setupExtensionSymlinks(cwd, extensionsToInstall);
|
|
1252
|
-
}
|
|
1253
|
-
// 12. 설치 검증
|
|
1254
|
-
const verificationResult = verifyInstallation(cwd, extensionsToInstall);
|
|
825
|
+
// 8. CLAUDE.md 생성
|
|
826
|
+
await setupClaudeMd(cwd, [], options.force);
|
|
827
|
+
// 9. 설치 검증
|
|
828
|
+
const verificationResult = verifyInstallation(cwd, []);
|
|
1255
829
|
printVerificationResult(verificationResult);
|
|
830
|
+
// 10. Skills DB 시딩 (--seed-skills 옵션)
|
|
831
|
+
if (options.seedSkills) {
|
|
832
|
+
console.log(chalk_1.default.cyan("\n🌱 스킬 DB 시딩 (--seed-skills)"));
|
|
833
|
+
const semoSystemDir = path.join(cwd, "semo-system");
|
|
834
|
+
await seedSkillsToDb(semoSystemDir);
|
|
835
|
+
}
|
|
1256
836
|
// 완료 메시지
|
|
1257
837
|
if (verificationResult.success) {
|
|
1258
838
|
console.log(chalk_1.default.green.bold("\n✅ SEMO 설치 완료!\n"));
|
|
@@ -1266,19 +846,10 @@ program
|
|
|
1266
846
|
console.log(chalk_1.default.gray(" ✓ semo-skills (13개 통합 스킬)"));
|
|
1267
847
|
console.log(chalk_1.default.gray(" ✓ semo-agents (14개 페르소나 Agent)"));
|
|
1268
848
|
console.log(chalk_1.default.gray(" ✓ semo-scripts (자동화 스크립트)"));
|
|
1269
|
-
if (extensionsToInstall.length > 0) {
|
|
1270
|
-
console.log(chalk_1.default.gray(" [Extensions]"));
|
|
1271
|
-
extensionsToInstall.forEach(pkg => {
|
|
1272
|
-
console.log(chalk_1.default.gray(` ✓ ${EXTENSION_PACKAGES[pkg].name}`));
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
849
|
console.log(chalk_1.default.cyan("\n다음 단계:"));
|
|
1276
850
|
console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기"));
|
|
1277
851
|
console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
|
|
1278
852
|
console.log(chalk_1.default.gray(" 3. /SEMO:help로 도움말 확인"));
|
|
1279
|
-
if (extensionsToInstall.length === 0) {
|
|
1280
|
-
console.log(chalk_1.default.gray("\n💡 추가 패키지가 필요하면: semo add <package> (예: semo add next)"));
|
|
1281
|
-
}
|
|
1282
853
|
console.log();
|
|
1283
854
|
});
|
|
1284
855
|
// === Standard 설치 (DB 기반) ===
|
|
@@ -1375,6 +946,24 @@ async function generateClaudeMd(cwd) {
|
|
|
1375
946
|
|
|
1376
947
|
---
|
|
1377
948
|
|
|
949
|
+
## 🔴 MANDATORY: Memory Context (항시 참조)
|
|
950
|
+
|
|
951
|
+
> **⚠️ 세션 시작 시 반드시 \`.claude/memory/\` 폴더의 파일들을 먼저 읽으세요. 예외 없음.**
|
|
952
|
+
|
|
953
|
+
### 필수 참조 파일
|
|
954
|
+
|
|
955
|
+
\`\`\`
|
|
956
|
+
.claude/memory/
|
|
957
|
+
├── context.md # 프로젝트 상태, 기술 스택, 진행 중 작업
|
|
958
|
+
├── decisions.md # 아키텍처 결정 기록 (ADR)
|
|
959
|
+
├── projects.md # GitHub Projects 설정
|
|
960
|
+
└── rules/ # 프로젝트별 커스텀 규칙
|
|
961
|
+
\`\`\`
|
|
962
|
+
|
|
963
|
+
**이 파일들은 세션의 컨텍스트를 유지하는 장기 기억입니다. 매 세션마다 반드시 읽고 시작하세요.**
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
1378
967
|
## 🔴 MANDATORY: Orchestrator-First Execution
|
|
1379
968
|
|
|
1380
969
|
> **⚠️ 이 규칙은 모든 사용자 요청에 적용됩니다. 예외 없음.**
|
|
@@ -1765,276 +1354,6 @@ function printVerificationResult(result) {
|
|
|
1765
1354
|
console.log(chalk_1.default.gray(" 'semo init --force'로 재설치하거나 수동으로 문제를 해결하세요."));
|
|
1766
1355
|
}
|
|
1767
1356
|
}
|
|
1768
|
-
// === Extensions 다운로드 (심볼릭 링크 제외) ===
|
|
1769
|
-
async function downloadExtensions(cwd, packages, force) {
|
|
1770
|
-
console.log(chalk_1.default.cyan("\n📦 Extensions 다운로드"));
|
|
1771
|
-
packages.forEach(pkg => {
|
|
1772
|
-
console.log(chalk_1.default.gray(` - ${EXTENSION_PACKAGES[pkg].name}`));
|
|
1773
|
-
});
|
|
1774
|
-
console.log();
|
|
1775
|
-
const spinner = (0, ora_1.default)("Extension 패키지 다운로드 중...").start();
|
|
1776
|
-
try {
|
|
1777
|
-
const tempDir = path.join(cwd, ".semo-temp");
|
|
1778
|
-
// 이미 temp가 없으면 clone
|
|
1779
|
-
if (!fs.existsSync(tempDir)) {
|
|
1780
|
-
(0, child_process_1.execSync)(`git clone --depth 1 ${SEMO_REPO} "${tempDir}"`, { stdio: "pipe" });
|
|
1781
|
-
}
|
|
1782
|
-
const semoSystemDir = path.join(cwd, "semo-system");
|
|
1783
|
-
// 그룹 추출 (중복 제거) - 그룹 레벨 CLAUDE.md 복사용
|
|
1784
|
-
const groups = [...new Set(packages.map(pkg => pkg.split("/")[0]).filter(g => ["biz", "eng", "ops"].includes(g)))];
|
|
1785
|
-
// 그룹 레벨 파일 복사 (CLAUDE.md, VERSION 등)
|
|
1786
|
-
for (const group of groups) {
|
|
1787
|
-
const groupSrcDir = path.join(tempDir, "packages", group);
|
|
1788
|
-
const groupDestDir = path.join(semoSystemDir, group);
|
|
1789
|
-
// 그룹 디렉토리의 루트 파일만 복사 (CLAUDE.md, VERSION)
|
|
1790
|
-
if (fs.existsSync(groupSrcDir)) {
|
|
1791
|
-
fs.mkdirSync(groupDestDir, { recursive: true });
|
|
1792
|
-
const groupFiles = fs.readdirSync(groupSrcDir);
|
|
1793
|
-
for (const file of groupFiles) {
|
|
1794
|
-
const srcFile = path.join(groupSrcDir, file);
|
|
1795
|
-
const destFile = path.join(groupDestDir, file);
|
|
1796
|
-
if (fs.statSync(srcFile).isFile()) {
|
|
1797
|
-
fs.copyFileSync(srcFile, destFile);
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
console.log(chalk_1.default.green(` ✓ ${group}/ 그룹 파일 복사 (CLAUDE.md 등)`));
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
// 개별 패키지 복사
|
|
1804
|
-
for (const pkg of packages) {
|
|
1805
|
-
const srcPath = path.join(tempDir, "packages", pkg);
|
|
1806
|
-
const destPath = path.join(semoSystemDir, pkg);
|
|
1807
|
-
if (fs.existsSync(srcPath)) {
|
|
1808
|
-
if (fs.existsSync(destPath) && !force) {
|
|
1809
|
-
console.log(chalk_1.default.yellow(` ⚠ ${pkg}/ 이미 존재 (건너뜀)`));
|
|
1810
|
-
continue;
|
|
1811
|
-
}
|
|
1812
|
-
removeRecursive(destPath);
|
|
1813
|
-
// 상위 디렉토리 생성 (biz/discovery -> biz/ 먼저 생성)
|
|
1814
|
-
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
1815
|
-
copyRecursive(srcPath, destPath);
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
removeRecursive(tempDir);
|
|
1819
|
-
spinner.succeed(`Extensions 다운로드 완료 (${packages.length}개)`);
|
|
1820
|
-
}
|
|
1821
|
-
catch (error) {
|
|
1822
|
-
spinner.fail("Extensions 다운로드 실패");
|
|
1823
|
-
console.error(chalk_1.default.red(` ${error}`));
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
// === Orchestrator 병합 파일 생성 ===
|
|
1827
|
-
function createMergedOrchestrator(claudeAgentsDir, orchestratorSources) {
|
|
1828
|
-
const orchestratorDir = path.join(claudeAgentsDir, "orchestrator");
|
|
1829
|
-
fs.mkdirSync(orchestratorDir, { recursive: true });
|
|
1830
|
-
// _packages 디렉토리 생성 (원본 참조용)
|
|
1831
|
-
const packagesDir = path.join(orchestratorDir, "_packages");
|
|
1832
|
-
fs.mkdirSync(packagesDir, { recursive: true });
|
|
1833
|
-
// 각 패키지의 orchestrator 내용 수집
|
|
1834
|
-
const routingTables = [];
|
|
1835
|
-
const availableAgents = [];
|
|
1836
|
-
const availableSkills = [];
|
|
1837
|
-
const crossPackageRouting = [];
|
|
1838
|
-
for (const source of orchestratorSources) {
|
|
1839
|
-
const orchestratorMdPath = path.join(source.path, "orchestrator.md");
|
|
1840
|
-
if (!fs.existsSync(orchestratorMdPath))
|
|
1841
|
-
continue;
|
|
1842
|
-
const content = fs.readFileSync(orchestratorMdPath, "utf-8");
|
|
1843
|
-
const pkgShortName = source.pkg.replace(/\//g, "-");
|
|
1844
|
-
// 원본 파일 복사 (참조용)
|
|
1845
|
-
fs.writeFileSync(path.join(packagesDir, `${pkgShortName}.md`), content);
|
|
1846
|
-
// Quick Routing Table 추출
|
|
1847
|
-
const routingMatch = content.match(/## 🔴 Quick Routing Table[\s\S]*?\n\n([\s\S]*?)(?=\n## |$)/);
|
|
1848
|
-
if (routingMatch) {
|
|
1849
|
-
routingTables.push(`### ${EXTENSION_PACKAGES[source.pkg]?.name || source.pkg}\n\n${routingMatch[1].trim()}`);
|
|
1850
|
-
}
|
|
1851
|
-
// Available Agents 추출
|
|
1852
|
-
const agentsMatch = content.match(/## Available Agents[\s\S]*?\n\n([\s\S]*?)(?=\n## |$)/);
|
|
1853
|
-
if (agentsMatch) {
|
|
1854
|
-
availableAgents.push(`### ${EXTENSION_PACKAGES[source.pkg]?.name || source.pkg}\n\n${agentsMatch[1].trim()}`);
|
|
1855
|
-
}
|
|
1856
|
-
// Available Skills 추출
|
|
1857
|
-
const skillsMatch = content.match(/## Available Skills[\s\S]*?\n\n([\s\S]*?)(?=\n## |$)/);
|
|
1858
|
-
if (skillsMatch) {
|
|
1859
|
-
availableSkills.push(`### ${EXTENSION_PACKAGES[source.pkg]?.name || source.pkg}\n\n${skillsMatch[1].trim()}`);
|
|
1860
|
-
}
|
|
1861
|
-
// Cross-Package Routing 추출
|
|
1862
|
-
const crossMatch = content.match(/## 🔄 Cross-Package Routing[\s\S]*?\n\n([\s\S]*?)(?=\n## |$)/);
|
|
1863
|
-
if (crossMatch) {
|
|
1864
|
-
crossPackageRouting.push(crossMatch[1].trim());
|
|
1865
|
-
}
|
|
1866
|
-
// references 폴더가 있으면 복사
|
|
1867
|
-
const refsDir = path.join(source.path, "references");
|
|
1868
|
-
if (fs.existsSync(refsDir)) {
|
|
1869
|
-
const mergedRefsDir = path.join(orchestratorDir, "references");
|
|
1870
|
-
fs.mkdirSync(mergedRefsDir, { recursive: true });
|
|
1871
|
-
const refs = fs.readdirSync(refsDir);
|
|
1872
|
-
for (const ref of refs) {
|
|
1873
|
-
const srcRef = path.join(refsDir, ref);
|
|
1874
|
-
const destRef = path.join(mergedRefsDir, `${pkgShortName}-${ref}`);
|
|
1875
|
-
if (fs.statSync(srcRef).isFile()) {
|
|
1876
|
-
fs.copyFileSync(srcRef, destRef);
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
// 병합된 orchestrator.md 생성
|
|
1882
|
-
const mergedContent = `---
|
|
1883
|
-
name: orchestrator
|
|
1884
|
-
description: |
|
|
1885
|
-
SEMO Merged Orchestrator - Routes all user requests to appropriate agents/skills.
|
|
1886
|
-
This orchestrator combines routing tables from ${orchestratorSources.length} packages.
|
|
1887
|
-
PROACTIVELY delegate on ALL requests. Never process directly.
|
|
1888
|
-
tools:
|
|
1889
|
-
- read_file
|
|
1890
|
-
- list_dir
|
|
1891
|
-
- run_command
|
|
1892
|
-
- glob
|
|
1893
|
-
- grep
|
|
1894
|
-
- task
|
|
1895
|
-
- skill
|
|
1896
|
-
model: inherit
|
|
1897
|
-
---
|
|
1898
|
-
|
|
1899
|
-
# SEMO Merged Orchestrator
|
|
1900
|
-
|
|
1901
|
-
> 이 파일은 **자동 생성**되었습니다. 직접 수정하지 마세요.
|
|
1902
|
-
> 원본 파일: \`_packages/\` 디렉토리 참조
|
|
1903
|
-
|
|
1904
|
-
모든 사용자 요청을 분석하고 적절한 Agent 또는 Skill로 라우팅하는 **Primary Router**입니다.
|
|
1905
|
-
|
|
1906
|
-
## 🔴 설치된 패키지
|
|
1907
|
-
|
|
1908
|
-
${orchestratorSources.map(s => `- **${EXTENSION_PACKAGES[s.pkg]?.name || s.pkg}**: \`semo-system/${s.pkg}\``).join("\n")}
|
|
1909
|
-
|
|
1910
|
-
## �� Quick Routing Table (Merged)
|
|
1911
|
-
|
|
1912
|
-
> 키워드 매칭 시 **첫 번째 매칭된 패키지**로 라우팅됩니다.
|
|
1913
|
-
|
|
1914
|
-
${routingTables.join("\n\n---\n\n")}
|
|
1915
|
-
|
|
1916
|
-
## SEMO 메시지 포맷
|
|
1917
|
-
|
|
1918
|
-
### Agent 위임
|
|
1919
|
-
|
|
1920
|
-
\`\`\`markdown
|
|
1921
|
-
[SEMO] Orchestrator: 의도 분석 완료 → {intent_category}
|
|
1922
|
-
|
|
1923
|
-
[SEMO] Agent 위임: {agent_name} (사유: {reason})
|
|
1924
|
-
\`\`\`
|
|
1925
|
-
|
|
1926
|
-
### Skill 호출
|
|
1927
|
-
|
|
1928
|
-
\`\`\`markdown
|
|
1929
|
-
[SEMO] Orchestrator: 의도 분석 완료 → {intent_category}
|
|
1930
|
-
|
|
1931
|
-
[SEMO] Skill 호출: {skill_name}
|
|
1932
|
-
\`\`\`
|
|
1933
|
-
|
|
1934
|
-
### 라우팅 실패
|
|
1935
|
-
|
|
1936
|
-
\`\`\`markdown
|
|
1937
|
-
[SEMO] Orchestrator: 라우팅 실패 → 적절한 Agent/Skill 없음
|
|
1938
|
-
|
|
1939
|
-
⚠️ 직접 처리 필요
|
|
1940
|
-
\`\`\`
|
|
1941
|
-
|
|
1942
|
-
## Critical Rules
|
|
1943
|
-
|
|
1944
|
-
1. **Routing-Only**: 직접 작업 수행 금지
|
|
1945
|
-
2. **SEMO 메시지 필수**: 모든 위임에 SEMO 메시지 포함
|
|
1946
|
-
3. **Package Priority**: 라우팅 충돌 시 설치 순서대로 우선순위 적용
|
|
1947
|
-
4. **Cross-Package**: 다른 패키지 전문 영역 요청 시 인계 권유
|
|
1948
|
-
|
|
1949
|
-
${crossPackageRouting.length > 0 ? `## 🔄 Cross-Package Routing
|
|
1950
|
-
|
|
1951
|
-
${crossPackageRouting[0]}` : ""}
|
|
1952
|
-
|
|
1953
|
-
${availableAgents.length > 0 ? `## Available Agents (All Packages)
|
|
1954
|
-
|
|
1955
|
-
${availableAgents.join("\n\n")}` : ""}
|
|
1956
|
-
|
|
1957
|
-
${availableSkills.length > 0 ? `## Available Skills (All Packages)
|
|
1958
|
-
|
|
1959
|
-
${availableSkills.join("\n\n")}` : ""}
|
|
1960
|
-
|
|
1961
|
-
## References
|
|
1962
|
-
|
|
1963
|
-
- 원본 Orchestrator: \`_packages/\` 디렉토리
|
|
1964
|
-
- 병합된 References: \`references/\` 디렉토리
|
|
1965
|
-
`;
|
|
1966
|
-
fs.writeFileSync(path.join(orchestratorDir, "orchestrator.md"), mergedContent);
|
|
1967
|
-
}
|
|
1968
|
-
// === Extensions 심볼릭 링크 설정 (agents/skills 병합) ===
|
|
1969
|
-
async function setupExtensionSymlinks(cwd, packages) {
|
|
1970
|
-
console.log(chalk_1.default.cyan("\n🔗 Extensions 연결"));
|
|
1971
|
-
const claudeDir = path.join(cwd, ".claude");
|
|
1972
|
-
const semoSystemDir = path.join(cwd, "semo-system");
|
|
1973
|
-
// .claude/agents, .claude/skills 디렉토리 생성 (없으면)
|
|
1974
|
-
const claudeAgentsDir = path.join(claudeDir, "agents");
|
|
1975
|
-
const claudeSkillsDir = path.join(claudeDir, "skills");
|
|
1976
|
-
fs.mkdirSync(claudeAgentsDir, { recursive: true });
|
|
1977
|
-
fs.mkdirSync(claudeSkillsDir, { recursive: true });
|
|
1978
|
-
// Orchestrator 소스 수집 (병합용)
|
|
1979
|
-
const orchestratorSources = [];
|
|
1980
|
-
for (const pkg of packages) {
|
|
1981
|
-
const pkgPath = path.join(semoSystemDir, pkg);
|
|
1982
|
-
if (!fs.existsSync(pkgPath))
|
|
1983
|
-
continue;
|
|
1984
|
-
// 1. Extension의 agents를 .claude/agents/에 개별 링크
|
|
1985
|
-
const extAgentsDir = path.join(pkgPath, "agents");
|
|
1986
|
-
if (fs.existsSync(extAgentsDir)) {
|
|
1987
|
-
const agents = fs.readdirSync(extAgentsDir).filter(f => fs.statSync(path.join(extAgentsDir, f)).isDirectory());
|
|
1988
|
-
for (const agent of agents) {
|
|
1989
|
-
const agentLink = path.join(claudeAgentsDir, agent);
|
|
1990
|
-
const agentTarget = path.join(extAgentsDir, agent);
|
|
1991
|
-
// Orchestrator는 특별 처리 (병합 필요)
|
|
1992
|
-
if (agent === "orchestrator") {
|
|
1993
|
-
orchestratorSources.push({ pkg, path: agentTarget });
|
|
1994
|
-
continue; // 심볼릭 링크 생성 안 함
|
|
1995
|
-
}
|
|
1996
|
-
if (!fs.existsSync(agentLink)) {
|
|
1997
|
-
createSymlinkOrJunction(agentTarget, agentLink);
|
|
1998
|
-
console.log(chalk_1.default.green(` ✓ .claude/agents/${agent} → semo-system/${pkg}/agents/${agent}`));
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
// 2. Extension의 skills를 .claude/skills/에 개별 링크
|
|
2003
|
-
const extSkillsDir = path.join(pkgPath, "skills");
|
|
2004
|
-
if (fs.existsSync(extSkillsDir)) {
|
|
2005
|
-
const skills = fs.readdirSync(extSkillsDir).filter(f => fs.statSync(path.join(extSkillsDir, f)).isDirectory());
|
|
2006
|
-
for (const skill of skills) {
|
|
2007
|
-
const skillLink = path.join(claudeSkillsDir, skill);
|
|
2008
|
-
const skillTarget = path.join(extSkillsDir, skill);
|
|
2009
|
-
if (!fs.existsSync(skillLink)) {
|
|
2010
|
-
createSymlinkOrJunction(skillTarget, skillLink);
|
|
2011
|
-
console.log(chalk_1.default.green(` ✓ .claude/skills/${skill} → semo-system/${pkg}/skills/${skill}`));
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
// 3. Orchestrator 병합 처리
|
|
2017
|
-
if (orchestratorSources.length > 0) {
|
|
2018
|
-
// 기존 orchestrator 링크/디렉토리 제거
|
|
2019
|
-
const orchestratorPath = path.join(claudeAgentsDir, "orchestrator");
|
|
2020
|
-
if (fs.existsSync(orchestratorPath)) {
|
|
2021
|
-
removeRecursive(orchestratorPath);
|
|
2022
|
-
}
|
|
2023
|
-
if (orchestratorSources.length === 1) {
|
|
2024
|
-
// 단일 패키지: 심볼릭 링크
|
|
2025
|
-
createSymlinkOrJunction(orchestratorSources[0].path, orchestratorPath);
|
|
2026
|
-
console.log(chalk_1.default.green(` ✓ .claude/agents/orchestrator → semo-system/${orchestratorSources[0].pkg}/agents/orchestrator`));
|
|
2027
|
-
}
|
|
2028
|
-
else {
|
|
2029
|
-
// 다중 패키지: 병합 파일 생성
|
|
2030
|
-
createMergedOrchestrator(claudeAgentsDir, orchestratorSources);
|
|
2031
|
-
console.log(chalk_1.default.green(` ✓ .claude/agents/orchestrator (${orchestratorSources.length}개 패키지 병합)`));
|
|
2032
|
-
for (const source of orchestratorSources) {
|
|
2033
|
-
console.log(chalk_1.default.gray(` - semo-system/${source.pkg}/agents/orchestrator`));
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
1357
|
const BASE_MCP_SERVERS = [
|
|
2039
1358
|
{
|
|
2040
1359
|
name: "semo-integrations",
|
|
@@ -2103,7 +1422,7 @@ function registerMCPServer(server) {
|
|
|
2103
1422
|
}
|
|
2104
1423
|
}
|
|
2105
1424
|
// === MCP 설정 ===
|
|
2106
|
-
async function setupMCP(cwd,
|
|
1425
|
+
async function setupMCP(cwd, _extensions, force) {
|
|
2107
1426
|
console.log(chalk_1.default.cyan("\n🔧 Black Box 설정 (MCP Server)"));
|
|
2108
1427
|
console.log(chalk_1.default.gray(" 토큰이 격리된 외부 연동 도구\n"));
|
|
2109
1428
|
const settingsPath = path.join(cwd, ".claude", "settings.json");
|
|
@@ -2120,51 +1439,6 @@ async function setupMCP(cwd, extensions, force) {
|
|
|
2120
1439
|
};
|
|
2121
1440
|
// MCP 서버 목록 수집
|
|
2122
1441
|
const allServers = [...BASE_MCP_SERVERS];
|
|
2123
|
-
// Extension settings 병합
|
|
2124
|
-
const semoSystemDir = path.join(cwd, "semo-system");
|
|
2125
|
-
for (const pkg of extensions) {
|
|
2126
|
-
const extSettingsPath = path.join(semoSystemDir, pkg, "settings.local.json");
|
|
2127
|
-
if (fs.existsSync(extSettingsPath)) {
|
|
2128
|
-
try {
|
|
2129
|
-
const extSettings = JSON.parse(fs.readFileSync(extSettingsPath, "utf-8"));
|
|
2130
|
-
// mcpServers 병합
|
|
2131
|
-
if (extSettings.mcpServers) {
|
|
2132
|
-
for (const [name, config] of Object.entries(extSettings.mcpServers)) {
|
|
2133
|
-
const serverConfig = config;
|
|
2134
|
-
allServers.push({
|
|
2135
|
-
name,
|
|
2136
|
-
command: serverConfig.command,
|
|
2137
|
-
args: serverConfig.args,
|
|
2138
|
-
env: serverConfig.env,
|
|
2139
|
-
});
|
|
2140
|
-
}
|
|
2141
|
-
console.log(chalk_1.default.gray(` + ${pkg} MCP 설정 수집됨`));
|
|
2142
|
-
}
|
|
2143
|
-
// permissions 병합
|
|
2144
|
-
if (extSettings.permissions) {
|
|
2145
|
-
if (!settings.permissions) {
|
|
2146
|
-
settings.permissions = { allow: [], deny: [] };
|
|
2147
|
-
}
|
|
2148
|
-
if (extSettings.permissions.allow) {
|
|
2149
|
-
settings.permissions.allow = [
|
|
2150
|
-
...(settings.permissions.allow || []),
|
|
2151
|
-
...extSettings.permissions.allow,
|
|
2152
|
-
];
|
|
2153
|
-
}
|
|
2154
|
-
if (extSettings.permissions.deny) {
|
|
2155
|
-
settings.permissions.deny = [
|
|
2156
|
-
...(settings.permissions.deny || []),
|
|
2157
|
-
...extSettings.permissions.deny,
|
|
2158
|
-
];
|
|
2159
|
-
}
|
|
2160
|
-
console.log(chalk_1.default.gray(` + ${pkg} permissions 병합됨`));
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
catch (error) {
|
|
2164
|
-
console.log(chalk_1.default.yellow(` ⚠ ${pkg} settings.local.json 파싱 실패`));
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
1442
|
// settings.json에 mcpServers 저장 (백업용)
|
|
2169
1443
|
for (const server of allServers) {
|
|
2170
1444
|
const serverConfig = {
|
|
@@ -2223,104 +1497,6 @@ async function setupMCP(cwd, extensions, force) {
|
|
|
2223
1497
|
console.log();
|
|
2224
1498
|
}
|
|
2225
1499
|
}
|
|
2226
|
-
// === Extension settings 병합 (add 명령어용) ===
|
|
2227
|
-
async function mergeExtensionSettings(cwd, packages) {
|
|
2228
|
-
const settingsPath = path.join(cwd, ".claude", "settings.json");
|
|
2229
|
-
const semoSystemDir = path.join(cwd, "semo-system");
|
|
2230
|
-
if (!fs.existsSync(settingsPath)) {
|
|
2231
|
-
console.log(chalk_1.default.yellow(" ⚠ settings.json이 없습니다. 'semo init'을 먼저 실행하세요."));
|
|
2232
|
-
return;
|
|
2233
|
-
}
|
|
2234
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
2235
|
-
const newServers = [];
|
|
2236
|
-
for (const pkg of packages) {
|
|
2237
|
-
const extSettingsPath = path.join(semoSystemDir, pkg, "settings.local.json");
|
|
2238
|
-
if (fs.existsSync(extSettingsPath)) {
|
|
2239
|
-
try {
|
|
2240
|
-
const extSettings = JSON.parse(fs.readFileSync(extSettingsPath, "utf-8"));
|
|
2241
|
-
// mcpServers 병합
|
|
2242
|
-
if (extSettings.mcpServers) {
|
|
2243
|
-
settings.mcpServers = settings.mcpServers || {};
|
|
2244
|
-
for (const [name, config] of Object.entries(extSettings.mcpServers)) {
|
|
2245
|
-
const serverConfig = config;
|
|
2246
|
-
settings.mcpServers[name] = serverConfig;
|
|
2247
|
-
newServers.push({
|
|
2248
|
-
name,
|
|
2249
|
-
command: serverConfig.command,
|
|
2250
|
-
args: serverConfig.args,
|
|
2251
|
-
env: serverConfig.env,
|
|
2252
|
-
});
|
|
2253
|
-
}
|
|
2254
|
-
console.log(chalk_1.default.gray(` + ${pkg} MCP 설정 병합됨`));
|
|
2255
|
-
}
|
|
2256
|
-
// permissions 병합
|
|
2257
|
-
if (extSettings.permissions) {
|
|
2258
|
-
settings.permissions = settings.permissions || { allow: [], deny: [] };
|
|
2259
|
-
if (extSettings.permissions.allow) {
|
|
2260
|
-
settings.permissions.allow = [
|
|
2261
|
-
...(settings.permissions.allow || []),
|
|
2262
|
-
...extSettings.permissions.allow,
|
|
2263
|
-
];
|
|
2264
|
-
}
|
|
2265
|
-
if (extSettings.permissions.deny) {
|
|
2266
|
-
settings.permissions.deny = [
|
|
2267
|
-
...(settings.permissions.deny || []),
|
|
2268
|
-
...extSettings.permissions.deny,
|
|
2269
|
-
];
|
|
2270
|
-
}
|
|
2271
|
-
console.log(chalk_1.default.gray(` + ${pkg} permissions 병합됨`));
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
catch (error) {
|
|
2275
|
-
console.log(chalk_1.default.yellow(` ⚠ ${pkg} settings.local.json 파싱 실패`));
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
2280
|
-
// 새 MCP 서버 Claude Code에 등록
|
|
2281
|
-
if (newServers.length > 0) {
|
|
2282
|
-
console.log(chalk_1.default.cyan("\n🔌 Claude Code에 MCP 서버 등록 중..."));
|
|
2283
|
-
const successServers = [];
|
|
2284
|
-
const skippedServers = [];
|
|
2285
|
-
const failedServers = [];
|
|
2286
|
-
for (const server of newServers) {
|
|
2287
|
-
const spinner = (0, ora_1.default)(` ${server.name} 등록 중...`).start();
|
|
2288
|
-
const result = registerMCPServer(server);
|
|
2289
|
-
if (result.success) {
|
|
2290
|
-
if (result.skipped) {
|
|
2291
|
-
spinner.info(` ${server.name} 이미 등록됨 (건너뜀)`);
|
|
2292
|
-
skippedServers.push(server.name);
|
|
2293
|
-
}
|
|
2294
|
-
else {
|
|
2295
|
-
spinner.succeed(` ${server.name} 등록 완료`);
|
|
2296
|
-
successServers.push(server.name);
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
else {
|
|
2300
|
-
spinner.fail(` ${server.name} 등록 실패`);
|
|
2301
|
-
failedServers.push(server);
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
if (successServers.length > 0) {
|
|
2305
|
-
console.log(chalk_1.default.green(`\n✓ ${successServers.length}개 MCP 서버 새로 등록 완료`));
|
|
2306
|
-
}
|
|
2307
|
-
if (skippedServers.length > 0) {
|
|
2308
|
-
console.log(chalk_1.default.gray(` (${skippedServers.length}개 이미 등록됨)`));
|
|
2309
|
-
}
|
|
2310
|
-
if (failedServers.length > 0) {
|
|
2311
|
-
console.log(chalk_1.default.yellow(`\n⚠ ${failedServers.length}개 MCP 서버 자동 등록 실패`));
|
|
2312
|
-
console.log(chalk_1.default.cyan("\n📋 수동 등록 명령어:"));
|
|
2313
|
-
for (const server of failedServers) {
|
|
2314
|
-
const envArgs = server.env
|
|
2315
|
-
? Object.entries(server.env).map(([k, v]) => `-e ${k}="${v}"`).join(" ")
|
|
2316
|
-
: "";
|
|
2317
|
-
const cmd = `claude mcp add ${server.name} ${envArgs} -- ${server.command} ${server.args.join(" ")}`.trim();
|
|
2318
|
-
console.log(chalk_1.default.white(` ${cmd}`));
|
|
2319
|
-
}
|
|
2320
|
-
console.log();
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
1500
|
// === .gitignore 업데이트 ===
|
|
2325
1501
|
function updateGitignore(cwd) {
|
|
2326
1502
|
console.log(chalk_1.default.cyan("\n📝 .gitignore 업데이트"));
|
|
@@ -2333,14 +1509,24 @@ function updateGitignore(cwd) {
|
|
|
2333
1509
|
semo-system/
|
|
2334
1510
|
`;
|
|
2335
1511
|
if (fs.existsSync(gitignorePath)) {
|
|
2336
|
-
|
|
1512
|
+
let content = fs.readFileSync(gitignorePath, "utf-8");
|
|
2337
1513
|
// 이미 SEMO 블록이 있으면 스킵
|
|
2338
1514
|
if (content.includes("# === SEMO ===")) {
|
|
2339
1515
|
console.log(chalk_1.default.gray(" → SEMO 블록 이미 존재 (건너뜀)"));
|
|
2340
1516
|
return;
|
|
2341
1517
|
}
|
|
1518
|
+
// 기존에 .claude/ 또는 .claude 전체 무시 항목 제거 (memory/ 접근을 위해)
|
|
1519
|
+
const lines = content.split("\n");
|
|
1520
|
+
const filtered = lines.filter(line => {
|
|
1521
|
+
const trimmed = line.trim();
|
|
1522
|
+
return trimmed !== ".claude" && trimmed !== ".claude/" && trimmed !== ".claude/**";
|
|
1523
|
+
});
|
|
1524
|
+
if (filtered.length !== lines.length) {
|
|
1525
|
+
content = filtered.join("\n");
|
|
1526
|
+
console.log(chalk_1.default.gray(" → 기존 .claude 무시 항목 제거됨 (memory/ 접근 허용)"));
|
|
1527
|
+
}
|
|
2342
1528
|
// 기존 파일에 추가
|
|
2343
|
-
fs.
|
|
1529
|
+
fs.writeFileSync(gitignorePath, content + semoIgnoreBlock);
|
|
2344
1530
|
console.log(chalk_1.default.green("✓ .gitignore에 SEMO 규칙 추가됨"));
|
|
2345
1531
|
}
|
|
2346
1532
|
else {
|
|
@@ -2401,6 +1587,16 @@ async function setupHooks(cwd, isUpdate = false) {
|
|
|
2401
1587
|
},
|
|
2402
1588
|
],
|
|
2403
1589
|
},
|
|
1590
|
+
{
|
|
1591
|
+
matcher: "",
|
|
1592
|
+
hooks: [
|
|
1593
|
+
{
|
|
1594
|
+
type: "command",
|
|
1595
|
+
command: "semo context sync 2>/dev/null || true",
|
|
1596
|
+
timeout: 30,
|
|
1597
|
+
},
|
|
1598
|
+
],
|
|
1599
|
+
},
|
|
2404
1600
|
],
|
|
2405
1601
|
UserPromptSubmit: [
|
|
2406
1602
|
{
|
|
@@ -2425,6 +1621,16 @@ async function setupHooks(cwd, isUpdate = false) {
|
|
|
2425
1621
|
},
|
|
2426
1622
|
],
|
|
2427
1623
|
},
|
|
1624
|
+
{
|
|
1625
|
+
matcher: "",
|
|
1626
|
+
hooks: [
|
|
1627
|
+
{
|
|
1628
|
+
type: "command",
|
|
1629
|
+
command: "semo context push 2>/dev/null || true",
|
|
1630
|
+
timeout: 30,
|
|
1631
|
+
},
|
|
1632
|
+
],
|
|
1633
|
+
},
|
|
2428
1634
|
],
|
|
2429
1635
|
SessionEnd: [
|
|
2430
1636
|
{
|
|
@@ -2628,69 +1834,8 @@ _SEMO 기본 규칙의 예외 사항을 여기에 추가하세요._
|
|
|
2628
1834
|
console.log(chalk_1.default.green("✓ .claude/memory/rules/project-specific.md 생성됨"));
|
|
2629
1835
|
}
|
|
2630
1836
|
}
|
|
2631
|
-
// === CLAUDE.md
|
|
2632
|
-
|
|
2633
|
-
function extractUniqueContent(content, pkgName) {
|
|
2634
|
-
// "Core Rules (상속)" 섹션이 있는지 확인
|
|
2635
|
-
const hasCoreRulesRef = /## Core Rules \(상속\)/i.test(content);
|
|
2636
|
-
if (hasCoreRulesRef) {
|
|
2637
|
-
// "고유:" 패턴이 포함된 섹션만 추출
|
|
2638
|
-
const uniqueSectionPattern = /## [^\n]* 고유:/g;
|
|
2639
|
-
const sections = [];
|
|
2640
|
-
// 섹션별로 분리
|
|
2641
|
-
const allSections = content.split(/(?=^## )/gm);
|
|
2642
|
-
for (const section of allSections) {
|
|
2643
|
-
// "고유:" 키워드가 있는 섹션만 포함
|
|
2644
|
-
if (/고유:/i.test(section)) {
|
|
2645
|
-
sections.push(section.trim());
|
|
2646
|
-
}
|
|
2647
|
-
// References 섹션도 포함
|
|
2648
|
-
if (/^## References/i.test(section)) {
|
|
2649
|
-
sections.push(section.trim());
|
|
2650
|
-
}
|
|
2651
|
-
// 패키지 구조, Keywords 섹션 포함
|
|
2652
|
-
if (/^## (패키지 구조|Keywords|Routing)/i.test(section)) {
|
|
2653
|
-
sections.push(section.trim());
|
|
2654
|
-
}
|
|
2655
|
-
}
|
|
2656
|
-
if (sections.length > 0) {
|
|
2657
|
-
return sections.join("\n\n");
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
// 공유 규칙 패턴 감지 (이 패턴이 있으면 중복 가능성 높음)
|
|
2661
|
-
const sharedPatterns = [
|
|
2662
|
-
/Orchestrator-First Policy/i,
|
|
2663
|
-
/Quality Gate|Pre-Commit/i,
|
|
2664
|
-
/세션 초기화|Session Init/i,
|
|
2665
|
-
/버저닝 규칙|Versioning/i,
|
|
2666
|
-
/패키지 접두사|PREFIX_ROUTING/i,
|
|
2667
|
-
/SEMO Core 필수 참조/i,
|
|
2668
|
-
/NON-NEGOTIABLE.*Orchestrator/i,
|
|
2669
|
-
];
|
|
2670
|
-
// 공유 패턴이 많이 발견되면 간소화된 참조만 반환
|
|
2671
|
-
let sharedPatternCount = 0;
|
|
2672
|
-
for (const pattern of sharedPatterns) {
|
|
2673
|
-
if (pattern.test(content)) {
|
|
2674
|
-
sharedPatternCount++;
|
|
2675
|
-
}
|
|
2676
|
-
}
|
|
2677
|
-
// 3개 이상의 공유 패턴이 발견되면 중복이 많은 것으로 판단
|
|
2678
|
-
if (sharedPatternCount >= 3) {
|
|
2679
|
-
// 기본 헤더와 References만 추출
|
|
2680
|
-
const headerMatch = content.match(/^# .+\n\n>[^\n]+/);
|
|
2681
|
-
const referencesMatch = content.match(/## References[\s\S]*$/);
|
|
2682
|
-
let simplified = headerMatch ? headerMatch[0] : `# ${pkgName}`;
|
|
2683
|
-
simplified += "\n\n> Core Rules는 semo-core/principles/를 참조합니다.";
|
|
2684
|
-
if (referencesMatch) {
|
|
2685
|
-
simplified += "\n\n" + referencesMatch[0];
|
|
2686
|
-
}
|
|
2687
|
-
return simplified;
|
|
2688
|
-
}
|
|
2689
|
-
// 그 외에는 전체 내용 반환
|
|
2690
|
-
return content;
|
|
2691
|
-
}
|
|
2692
|
-
// === CLAUDE.md 생성 (패키지 CLAUDE.md 병합 지원 + 중복 제거) ===
|
|
2693
|
-
async function setupClaudeMd(cwd, extensions, force) {
|
|
1837
|
+
// === CLAUDE.md 생성 ===
|
|
1838
|
+
async function setupClaudeMd(cwd, _extensions, force) {
|
|
2694
1839
|
console.log(chalk_1.default.cyan("\n📄 CLAUDE.md 설정"));
|
|
2695
1840
|
const claudeMdPath = path.join(cwd, ".claude", "CLAUDE.md");
|
|
2696
1841
|
if (fs.existsSync(claudeMdPath) && !force) {
|
|
@@ -2700,105 +1845,7 @@ async function setupClaudeMd(cwd, extensions, force) {
|
|
|
2700
1845
|
return;
|
|
2701
1846
|
}
|
|
2702
1847
|
}
|
|
2703
|
-
const
|
|
2704
|
-
const extensionsList = extensions.length > 0
|
|
2705
|
-
? extensions.map(pkg => `├── ${pkg}/ # ${EXTENSION_PACKAGES[pkg].name}`).join("\n")
|
|
2706
|
-
: "";
|
|
2707
|
-
// 그룹 및 패키지별 CLAUDE.md 병합 섹션 생성
|
|
2708
|
-
let packageClaudeMdSections = "";
|
|
2709
|
-
// 0. meta 패키지가 설치된 경우 먼저 확인 (meta는 특별 처리)
|
|
2710
|
-
const isMetaInstalled = extensions.includes("meta") ||
|
|
2711
|
-
fs.existsSync(path.join(semoSystemDir, "meta", "VERSION"));
|
|
2712
|
-
// 1. 설치된 패키지에서 그룹 추출 (중복 제거)
|
|
2713
|
-
const installedGroups = [...new Set(extensions.map(pkg => pkg.split("/")[0]).filter(g => PACKAGE_GROUPS.includes(g)))];
|
|
2714
|
-
// 2. 그룹 레벨 CLAUDE.md 먼저 병합 (biz, eng, ops) - 중복 제거 적용
|
|
2715
|
-
for (const group of installedGroups) {
|
|
2716
|
-
const groupClaudeMdPath = path.join(semoSystemDir, group, "CLAUDE.md");
|
|
2717
|
-
if (fs.existsSync(groupClaudeMdPath)) {
|
|
2718
|
-
const groupContent = fs.readFileSync(groupClaudeMdPath, "utf-8");
|
|
2719
|
-
// 중복 제거 후 고유 콘텐츠만 추출
|
|
2720
|
-
const uniqueContent = extractUniqueContent(groupContent, group);
|
|
2721
|
-
// 헤더 레벨 조정 (# → ##, ## → ###)
|
|
2722
|
-
const adjustedContent = uniqueContent
|
|
2723
|
-
.replace(/^# /gm, "## ")
|
|
2724
|
-
.replace(/^## /gm, "### ")
|
|
2725
|
-
.replace(/^### /gm, "#### ");
|
|
2726
|
-
packageClaudeMdSections += `\n\n---\n\n${adjustedContent}`;
|
|
2727
|
-
console.log(chalk_1.default.green(` + ${group}/ 그룹 CLAUDE.md 병합됨 (고유 섹션만)`));
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
// 3. 개별 패키지 CLAUDE.md 병합 - 중복 제거 적용
|
|
2731
|
-
for (const pkg of extensions) {
|
|
2732
|
-
// meta 패키지는 별도 처리 (아래에서 전체 내용 병합)
|
|
2733
|
-
if (pkg === "meta")
|
|
2734
|
-
continue;
|
|
2735
|
-
const pkgClaudeMdPath = path.join(semoSystemDir, pkg, "CLAUDE.md");
|
|
2736
|
-
if (fs.existsSync(pkgClaudeMdPath)) {
|
|
2737
|
-
const pkgContent = fs.readFileSync(pkgClaudeMdPath, "utf-8");
|
|
2738
|
-
const pkgName = EXTENSION_PACKAGES[pkg]?.name || pkg;
|
|
2739
|
-
// 중복 제거 후 고유 콘텐츠만 추출
|
|
2740
|
-
const uniqueContent = extractUniqueContent(pkgContent, pkgName);
|
|
2741
|
-
// 헤더 레벨 조정
|
|
2742
|
-
const adjustedContent = uniqueContent
|
|
2743
|
-
.replace(/^# /gm, "### ")
|
|
2744
|
-
.replace(/^## /gm, "#### ");
|
|
2745
|
-
packageClaudeMdSections += `\n\n---\n\n## ${pkgName} 패키지 컨텍스트\n\n${adjustedContent}`;
|
|
2746
|
-
console.log(chalk_1.default.gray(` + ${pkg}/CLAUDE.md 병합됨 (고유 섹션만)`));
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
// 3.5. meta 패키지 CLAUDE.md 병합 (전체 내용 - Meta 환경 규칙 포함)
|
|
2750
|
-
if (isMetaInstalled) {
|
|
2751
|
-
const metaClaudeMdPath = path.join(semoSystemDir, "meta", "CLAUDE.md");
|
|
2752
|
-
if (fs.existsSync(metaClaudeMdPath)) {
|
|
2753
|
-
const metaContent = fs.readFileSync(metaClaudeMdPath, "utf-8");
|
|
2754
|
-
const pkgName = EXTENSION_PACKAGES["meta"]?.name || "Meta";
|
|
2755
|
-
// meta는 중복 제거 없이 전체 내용 유지 (Core Rules가 중요)
|
|
2756
|
-
// 헤더 레벨만 조정 (# → ###, ## → ####)
|
|
2757
|
-
const adjustedContent = metaContent
|
|
2758
|
-
.replace(/^# /gm, "### ")
|
|
2759
|
-
.replace(/^## /gm, "#### ");
|
|
2760
|
-
packageClaudeMdSections += `\n\n---\n\n## ${pkgName} 패키지 컨텍스트\n\n${adjustedContent}`;
|
|
2761
|
-
console.log(chalk_1.default.green(` + meta/CLAUDE.md 병합됨 (전체 내용 - Meta 환경 규칙 포함)`));
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
// 4. Orchestrator 참조 경로 결정 (Extension 패키지 우선, meta 포함)
|
|
2765
|
-
// Extension 패키지 중 orchestrator가 있는 첫 번째 패키지를 Primary로 설정
|
|
2766
|
-
let primaryOrchestratorPath = "semo-core/agents/orchestrator/orchestrator.md";
|
|
2767
|
-
const orchestratorPaths = [];
|
|
2768
|
-
// meta 패키지 orchestrator 먼저 확인 (meta가 설치되어 있으면 최우선)
|
|
2769
|
-
if (isMetaInstalled) {
|
|
2770
|
-
const metaOrchestratorPath = path.join(semoSystemDir, "meta", "agents/orchestrator/orchestrator.md");
|
|
2771
|
-
if (fs.existsSync(metaOrchestratorPath)) {
|
|
2772
|
-
orchestratorPaths.push("semo-system/meta/agents/orchestrator/orchestrator.md");
|
|
2773
|
-
primaryOrchestratorPath = "meta/agents/orchestrator/orchestrator.md";
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
// 나머지 Extension 패키지 orchestrator 확인
|
|
2777
|
-
for (const pkg of extensions) {
|
|
2778
|
-
if (pkg === "meta")
|
|
2779
|
-
continue; // meta는 위에서 이미 처리
|
|
2780
|
-
const pkgOrchestratorPath = path.join(semoSystemDir, pkg, "agents/orchestrator/orchestrator.md");
|
|
2781
|
-
if (fs.existsSync(pkgOrchestratorPath)) {
|
|
2782
|
-
orchestratorPaths.push(`semo-system/${pkg}/agents/orchestrator/orchestrator.md`);
|
|
2783
|
-
// Primary가 아직 semo-core이면 이 패키지를 Primary로 설정
|
|
2784
|
-
if (primaryOrchestratorPath === "semo-core/agents/orchestrator/orchestrator.md") {
|
|
2785
|
-
primaryOrchestratorPath = `${pkg}/agents/orchestrator/orchestrator.md`;
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
// semo-core orchestrator는 항상 마지막에 포함 (fallback)
|
|
2790
|
-
orchestratorPaths.push("semo-system/semo-core/agents/orchestrator/orchestrator.md");
|
|
2791
|
-
// Orchestrator 참조 섹션 생성
|
|
2792
|
-
const orchestratorRefSection = orchestratorPaths.length > 1
|
|
2793
|
-
? `**Primary Orchestrator**: \`semo-system/${primaryOrchestratorPath}\`
|
|
2794
|
-
|
|
2795
|
-
> Extension 패키지가 설치되어 해당 패키지의 Orchestrator를 우선 참조합니다.
|
|
2796
|
-
|
|
2797
|
-
**모든 Orchestrator 파일** (라우팅 테이블 병합됨):
|
|
2798
|
-
${orchestratorPaths.map(p => `- \`${p}\``).join("\n")}
|
|
2799
|
-
|
|
2800
|
-
이 파일들에서 라우팅 테이블, 의도 분류, 메시지 포맷을 확인하세요.`
|
|
2801
|
-
: `**반드시 읽어야 할 파일**: \`semo-system/semo-core/agents/orchestrator/orchestrator.md\`
|
|
1848
|
+
const orchestratorRefSection = `**반드시 읽어야 할 파일**: \`semo-system/semo-core/agents/orchestrator/orchestrator.md\`
|
|
2802
1849
|
|
|
2803
1850
|
이 파일에서 라우팅 테이블, 의도 분류, 메시지 포맷을 확인하세요.`;
|
|
2804
1851
|
const claudeMdContent = `# SEMO Project Configuration
|
|
@@ -2807,6 +1854,24 @@ ${orchestratorPaths.map(p => `- \`${p}\``).join("\n")}
|
|
|
2807
1854
|
|
|
2808
1855
|
---
|
|
2809
1856
|
|
|
1857
|
+
## 🔴 MANDATORY: Memory Context (항시 참조)
|
|
1858
|
+
|
|
1859
|
+
> **⚠️ 세션 시작 시 반드시 \`.claude/memory/\` 폴더의 파일들을 먼저 읽으세요. 예외 없음.**
|
|
1860
|
+
|
|
1861
|
+
### 필수 참조 파일
|
|
1862
|
+
|
|
1863
|
+
\`\`\`
|
|
1864
|
+
.claude/memory/
|
|
1865
|
+
├── context.md # 프로젝트 상태, 기술 스택, 진행 중 작업
|
|
1866
|
+
├── decisions.md # 아키텍처 결정 기록 (ADR)
|
|
1867
|
+
├── projects.md # GitHub Projects 설정
|
|
1868
|
+
└── rules/ # 프로젝트별 커스텀 규칙
|
|
1869
|
+
\`\`\`
|
|
1870
|
+
|
|
1871
|
+
**이 파일들은 세션의 컨텍스트를 유지하는 장기 기억입니다. 매 세션마다 반드시 읽고 시작하세요.**
|
|
1872
|
+
|
|
1873
|
+
---
|
|
1874
|
+
|
|
2810
1875
|
## 🔴 MANDATORY: Orchestrator-First Execution
|
|
2811
1876
|
|
|
2812
1877
|
> **⚠️ 이 규칙은 모든 사용자 요청에 적용됩니다. 예외 없음.**
|
|
@@ -2854,46 +1919,7 @@ npm run build # 3. 빌드 검증 (Next.js/TypeScript 프로젝트)
|
|
|
2854
1919
|
- \`--no-verify\` 플래그 사용 금지
|
|
2855
1920
|
- Quality Gate 우회 시도 거부
|
|
2856
1921
|
- "그냥 커밋해줘", "빌드 생략해줘" 등 거부
|
|
2857
|
-
${isMetaInstalled ? `
|
|
2858
|
-
### 3. Meta 환경 자동 워크플로우 (NON-NEGOTIABLE)
|
|
2859
|
-
|
|
2860
|
-
> **⚠️ Meta 패키지가 설치된 환경에서는 반드시 아래 규칙이 적용됩니다.**
|
|
2861
|
-
> **이 규칙을 우회하거나 무시하는 것은 금지됩니다.**
|
|
2862
|
-
|
|
2863
|
-
#### 자동 트리거 조건
|
|
2864
|
-
|
|
2865
|
-
\`semo-system/\` 디렉토리 내 파일이 수정되면:
|
|
2866
|
-
1. 작업 종료 전 반드시 \`skill:meta-workflow\` 호출
|
|
2867
|
-
2. 버저닝 → 배포 → 로컬 동기화 체인 자동 실행
|
|
2868
|
-
|
|
2869
|
-
#### 감지 패턴
|
|
2870
|
-
|
|
2871
|
-
다음 경로의 파일 수정 시 자동 트리거:
|
|
2872
|
-
- \`semo-system/semo-core/**\`
|
|
2873
|
-
- \`semo-system/semo-skills/**\`
|
|
2874
|
-
- \`semo-system/meta/**\`
|
|
2875
|
-
- \`semo-system/semo-remote/**\`
|
|
2876
|
-
- \`semo-system/semo-hooks/**\`
|
|
2877
|
-
- \`packages/cli/**\` (CLI 수정 시)
|
|
2878
|
-
|
|
2879
|
-
#### 강제 동작 흐름
|
|
2880
|
-
|
|
2881
|
-
\`\`\`text
|
|
2882
|
-
[작업 완료 감지]
|
|
2883
|
-
↓
|
|
2884
|
-
semo-system/ 또는 packages/ 파일 수정 여부 확인
|
|
2885
|
-
↓
|
|
2886
|
-
수정됨? → [SEMO] Skill 호출: meta-workflow
|
|
2887
|
-
버저닝 → 배포 → 동기화 자동 실행
|
|
2888
|
-
↓
|
|
2889
|
-
수정 안됨? → 정상 종료
|
|
2890
|
-
\`\`\`
|
|
2891
1922
|
|
|
2892
|
-
**금지 사항**:
|
|
2893
|
-
- semo-system/ 수정 후 버저닝 없이 종료
|
|
2894
|
-
- "버저닝 나중에 해줘" 요청 수락
|
|
2895
|
-
- meta-workflow 스킬 호출 건너뛰기
|
|
2896
|
-
` : ``}
|
|
2897
1923
|
---
|
|
2898
1924
|
|
|
2899
1925
|
## 설치된 구성
|
|
@@ -2904,9 +1930,6 @@ semo-system/ 또는 packages/ 파일 수정 여부 확인
|
|
|
2904
1930
|
- 행동: coder, tester, planner, deployer, writer
|
|
2905
1931
|
- 운영: memory, notify-slack, feedback, version-updater, semo-help, semo-architecture-checker, circuit-breaker, list-bugs
|
|
2906
1932
|
|
|
2907
|
-
${extensions.length > 0 ? `### Extensions (선택)
|
|
2908
|
-
${extensions.map(pkg => `- **${pkg}**: ${EXTENSION_PACKAGES[pkg].desc}`).join("\n")}` : ""}
|
|
2909
|
-
|
|
2910
1933
|
## 구조
|
|
2911
1934
|
|
|
2912
1935
|
\`\`\`
|
|
@@ -2922,8 +1945,7 @@ ${extensions.map(pkg => `- **${pkg}**: ${EXTENSION_PACKAGES[pkg].desc}`).join("\
|
|
|
2922
1945
|
|
|
2923
1946
|
semo-system/ # White Box (읽기 전용)
|
|
2924
1947
|
├── semo-core/ # Layer 0: 원칙, 오케스트레이션
|
|
2925
|
-
|
|
2926
|
-
${extensionsList}
|
|
1948
|
+
└── semo-skills/ # Layer 1: 통합 스킬
|
|
2927
1949
|
\`\`\`
|
|
2928
1950
|
|
|
2929
1951
|
## 사용 가능한 커맨드
|
|
@@ -2950,167 +1972,40 @@ memory 스킬이 자동으로 이 파일들을 관리합니다.
|
|
|
2950
1972
|
|
|
2951
1973
|
- [SEMO Principles](semo-system/semo-core/principles/PRINCIPLES.md)
|
|
2952
1974
|
- [SEMO Skills](semo-system/semo-skills/)
|
|
2953
|
-
${extensions.length > 0 ? extensions.map(pkg => `- [${EXTENSION_PACKAGES[pkg].name} Package](semo-system/${pkg}/)`).join("\n") : ""}
|
|
2954
|
-
${packageClaudeMdSections}
|
|
2955
1975
|
`;
|
|
2956
1976
|
fs.writeFileSync(claudeMdPath, claudeMdContent);
|
|
2957
1977
|
console.log(chalk_1.default.green("✓ .claude/CLAUDE.md 생성됨"));
|
|
2958
|
-
if (packageClaudeMdSections) {
|
|
2959
|
-
console.log(chalk_1.default.green(` + ${extensions.length}개 패키지 CLAUDE.md 병합 완료`));
|
|
2960
|
-
}
|
|
2961
1978
|
}
|
|
2962
|
-
// === add 명령어 ===
|
|
2963
|
-
program
|
|
2964
|
-
.command("add <packages>")
|
|
2965
|
-
.description("Extension 패키지를 추가로 설치합니다 (그룹: biz, eng, ops, system / 개별: biz/discovery, eng/nextjs, semo-hooks)")
|
|
2966
|
-
.option("-f, --force", "기존 설정 덮어쓰기")
|
|
2967
|
-
.action(async (packagesInput, options) => {
|
|
2968
|
-
// 패키지 데이터 초기화 (DB에서 조회)
|
|
2969
|
-
await initPackageData();
|
|
2970
|
-
const cwd = process.cwd();
|
|
2971
|
-
const semoSystemDir = path.join(cwd, "semo-system");
|
|
2972
|
-
if (!fs.existsSync(semoSystemDir)) {
|
|
2973
|
-
console.log(chalk_1.default.red("\nSEMO가 설치되어 있지 않습니다. 'semo init'을 먼저 실행하세요.\n"));
|
|
2974
|
-
process.exit(1);
|
|
2975
|
-
}
|
|
2976
|
-
// 패키지 입력 해석 (그룹, 레거시, 쉼표 구분 모두 처리)
|
|
2977
|
-
const { packages, isGroup, groupName } = resolvePackageInput(packagesInput);
|
|
2978
|
-
const extPkgs = getExtensionPackagesSync();
|
|
2979
|
-
const shortnames = getShortnameMappingSync();
|
|
2980
|
-
if (packages.length === 0) {
|
|
2981
|
-
console.log(chalk_1.default.red(`\n알 수 없는 패키지: ${packagesInput}`));
|
|
2982
|
-
console.log(chalk_1.default.gray(`사용 가능한 그룹: ${PACKAGE_GROUPS.join(", ")}`));
|
|
2983
|
-
console.log(chalk_1.default.gray(`사용 가능한 패키지: ${Object.keys(extPkgs).join(", ")}`));
|
|
2984
|
-
console.log(chalk_1.default.gray(`단축명: ${Object.keys(shortnames).join(", ")}\n`));
|
|
2985
|
-
process.exit(1);
|
|
2986
|
-
}
|
|
2987
|
-
// 그룹 설치인 경우 안내
|
|
2988
|
-
if (isGroup) {
|
|
2989
|
-
console.log(chalk_1.default.cyan.bold(`\n📦 ${groupName?.toUpperCase()} 그룹 패키지 일괄 설치\n`));
|
|
2990
|
-
console.log(chalk_1.default.gray(" 포함된 패키지:"));
|
|
2991
|
-
for (const pkg of packages) {
|
|
2992
|
-
const pkgInfo = extPkgs[pkg];
|
|
2993
|
-
console.log(chalk_1.default.gray(` - ${pkg} (${pkgInfo?.name || pkg})`));
|
|
2994
|
-
}
|
|
2995
|
-
console.log();
|
|
2996
|
-
}
|
|
2997
|
-
else if (packages.length === 1) {
|
|
2998
|
-
// 단일 패키지
|
|
2999
|
-
const pkg = packages[0];
|
|
3000
|
-
const pkgInfo = extPkgs[pkg];
|
|
3001
|
-
console.log(chalk_1.default.cyan(`\n📦 ${pkgInfo?.name || pkg} 패키지 설치\n`));
|
|
3002
|
-
console.log(chalk_1.default.gray(` ${pkgInfo?.desc || ""}\n`));
|
|
3003
|
-
}
|
|
3004
|
-
else {
|
|
3005
|
-
// 여러 패키지 (쉼표 구분)
|
|
3006
|
-
console.log(chalk_1.default.cyan.bold(`\n📦 ${packages.length}개 패키지 설치\n`));
|
|
3007
|
-
for (const pkg of packages) {
|
|
3008
|
-
const pkgInfo = extPkgs[pkg];
|
|
3009
|
-
console.log(chalk_1.default.gray(` - ${pkg} (${pkgInfo?.name || pkg})`));
|
|
3010
|
-
}
|
|
3011
|
-
console.log();
|
|
3012
|
-
}
|
|
3013
|
-
// 기존에 설치된 모든 Extension 패키지 스캔
|
|
3014
|
-
const previouslyInstalled = getInstalledExtensions(cwd);
|
|
3015
|
-
// 요청한 패키지 중 이미 설치된 것과 새로 설치할 것 분류
|
|
3016
|
-
const alreadyInstalled = [];
|
|
3017
|
-
const toInstall = [];
|
|
3018
|
-
for (const pkg of packages) {
|
|
3019
|
-
const pkgPath = path.join(semoSystemDir, pkg);
|
|
3020
|
-
if (fs.existsSync(pkgPath) && !options.force) {
|
|
3021
|
-
alreadyInstalled.push(pkg);
|
|
3022
|
-
}
|
|
3023
|
-
else {
|
|
3024
|
-
toInstall.push(pkg);
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
if (alreadyInstalled.length > 0) {
|
|
3028
|
-
console.log(chalk_1.default.yellow("⚠ 이미 설치된 패키지 (건너뜀):"));
|
|
3029
|
-
for (const pkg of alreadyInstalled) {
|
|
3030
|
-
console.log(chalk_1.default.yellow(` - ${pkg}`));
|
|
3031
|
-
}
|
|
3032
|
-
console.log(chalk_1.default.gray(" 강제 재설치: semo add " + packagesInput + " --force\n"));
|
|
3033
|
-
}
|
|
3034
|
-
if (toInstall.length === 0) {
|
|
3035
|
-
console.log(chalk_1.default.yellow("\n모든 패키지가 이미 설치되어 있습니다.\n"));
|
|
3036
|
-
return;
|
|
3037
|
-
}
|
|
3038
|
-
// 1. 다운로드
|
|
3039
|
-
await downloadExtensions(cwd, toInstall, options.force);
|
|
3040
|
-
// 2. settings.json 병합
|
|
3041
|
-
await mergeExtensionSettings(cwd, toInstall);
|
|
3042
|
-
// 3. 심볼릭 링크 설정 (기존 + 새로 설치한 모든 패키지 포함)
|
|
3043
|
-
const allInstalledPackages = [...new Set([...previouslyInstalled, ...toInstall])];
|
|
3044
|
-
await setupExtensionSymlinks(cwd, allInstalledPackages);
|
|
3045
|
-
// 4. CLAUDE.md 재생성 (모든 설치된 패키지 반영)
|
|
3046
|
-
await setupClaudeMd(cwd, allInstalledPackages, options.force);
|
|
3047
|
-
if (toInstall.length === 1) {
|
|
3048
|
-
const pkgInfo = extPkgs[toInstall[0]];
|
|
3049
|
-
console.log(chalk_1.default.green.bold(`\n✅ ${pkgInfo?.name || toInstall[0]} 패키지 설치 완료!\n`));
|
|
3050
|
-
}
|
|
3051
|
-
else {
|
|
3052
|
-
console.log(chalk_1.default.green.bold(`\n✅ ${toInstall.length}개 패키지 설치 완료!`));
|
|
3053
|
-
for (const pkg of toInstall) {
|
|
3054
|
-
const pkgInfo = extPkgs[pkg];
|
|
3055
|
-
console.log(chalk_1.default.green(` ✓ ${pkgInfo?.name || pkg}`));
|
|
3056
|
-
}
|
|
3057
|
-
console.log();
|
|
3058
|
-
}
|
|
3059
|
-
});
|
|
3060
1979
|
// === list 명령어 ===
|
|
3061
1980
|
program
|
|
3062
1981
|
.command("list")
|
|
3063
|
-
.description("
|
|
1982
|
+
.description("설치된 SEMO 패키지 상태를 표시합니다")
|
|
3064
1983
|
.action(async () => {
|
|
3065
|
-
// 패키지 데이터 초기화 (DB에서 조회)
|
|
3066
|
-
await initPackageData();
|
|
3067
1984
|
const cwd = process.cwd();
|
|
3068
1985
|
const semoSystemDir = path.join(cwd, "semo-system");
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
// Standard
|
|
1986
|
+
console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록\n"));
|
|
1987
|
+
// Standard 패키지 표시
|
|
3072
1988
|
console.log(chalk_1.default.white.bold("Standard (필수)"));
|
|
3073
|
-
const
|
|
3074
|
-
const
|
|
3075
|
-
|
|
3076
|
-
|
|
1989
|
+
const standardPkgs = ["semo-core", "semo-skills", "semo-agents", "semo-scripts"];
|
|
1990
|
+
for (const pkg of standardPkgs) {
|
|
1991
|
+
const isInstalled = fs.existsSync(path.join(semoSystemDir, pkg));
|
|
1992
|
+
console.log(` ${isInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} ${pkg}`);
|
|
1993
|
+
}
|
|
3077
1994
|
console.log();
|
|
3078
|
-
//
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
const layerPackages = Object.entries(extPkgs).filter(([, pkg]) => pkg.layer === layerKey);
|
|
3088
|
-
if (layerPackages.length === 0)
|
|
3089
|
-
continue;
|
|
3090
|
-
console.log(chalk_1.default.white.bold(`${layerInfo.emoji} ${layerInfo.title}`));
|
|
3091
|
-
for (const [key, pkg] of layerPackages) {
|
|
3092
|
-
const isInstalled = fs.existsSync(path.join(semoSystemDir, key));
|
|
3093
|
-
const status = isInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○");
|
|
3094
|
-
const displayKey = key.includes("/") ? key.split("/")[1] : key;
|
|
3095
|
-
console.log(` ${status} ${chalk_1.default.cyan(displayKey)} - ${pkg.desc}`);
|
|
3096
|
-
console.log(chalk_1.default.gray(` semo add ${key}`));
|
|
1995
|
+
// DB 패키지 목록 (DB 연결 가능 시)
|
|
1996
|
+
try {
|
|
1997
|
+
const packages = await (0, database_1.getPackages)();
|
|
1998
|
+
if (packages.length > 0) {
|
|
1999
|
+
console.log(chalk_1.default.white.bold("DB 패키지"));
|
|
2000
|
+
for (const pkg of packages) {
|
|
2001
|
+
console.log(` ${chalk_1.default.cyan(pkg.name)} - ${pkg.description || ""}`);
|
|
2002
|
+
}
|
|
2003
|
+
console.log();
|
|
3097
2004
|
}
|
|
3098
|
-
console.log();
|
|
3099
2005
|
}
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
console.log(chalk_1.default.gray(" semo add biz → Business 전체 (discovery, design, management, poc)"));
|
|
3104
|
-
console.log(chalk_1.default.gray(" semo add eng → Engineering 전체 (nextjs, spring, ms, infra)"));
|
|
3105
|
-
console.log(chalk_1.default.gray(" semo add ops → Operations 전체 (qa, monitor, improve)"));
|
|
3106
|
-
console.log(chalk_1.default.gray(" semo add system → System 전체 (hooks, remote)"));
|
|
3107
|
-
console.log();
|
|
3108
|
-
// 단축명 안내
|
|
3109
|
-
console.log(chalk_1.default.gray("─".repeat(50)));
|
|
3110
|
-
console.log(chalk_1.default.white.bold("⚡ 단축명 지원"));
|
|
3111
|
-
console.log(chalk_1.default.gray(" semo add discovery → biz/discovery"));
|
|
3112
|
-
console.log(chalk_1.default.gray(" semo add qa → ops/qa"));
|
|
3113
|
-
console.log(chalk_1.default.gray(" semo add nextjs → eng/nextjs\n"));
|
|
2006
|
+
catch {
|
|
2007
|
+
// DB 연결 실패 시 무시
|
|
2008
|
+
}
|
|
3114
2009
|
});
|
|
3115
2010
|
// === status 명령어 ===
|
|
3116
2011
|
program
|
|
@@ -3133,19 +2028,6 @@ program
|
|
|
3133
2028
|
if (!exists)
|
|
3134
2029
|
standardOk = false;
|
|
3135
2030
|
}
|
|
3136
|
-
// Extensions 확인
|
|
3137
|
-
const installedExtensions = [];
|
|
3138
|
-
for (const key of Object.keys(EXTENSION_PACKAGES)) {
|
|
3139
|
-
if (fs.existsSync(path.join(semoSystemDir, key))) {
|
|
3140
|
-
installedExtensions.push(key);
|
|
3141
|
-
}
|
|
3142
|
-
}
|
|
3143
|
-
if (installedExtensions.length > 0) {
|
|
3144
|
-
console.log(chalk_1.default.white.bold("\nExtensions:"));
|
|
3145
|
-
for (const pkg of installedExtensions) {
|
|
3146
|
-
console.log(chalk_1.default.green(` ✓ ${pkg}`));
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
2031
|
// 구조 확인
|
|
3150
2032
|
console.log(chalk_1.default.white.bold("\n구조:"));
|
|
3151
2033
|
const structureChecks = [
|
|
@@ -3181,8 +2063,6 @@ program
|
|
|
3181
2063
|
.option("--migrate", "레거시 환경 강제 마이그레이션")
|
|
3182
2064
|
.action(async (options) => {
|
|
3183
2065
|
console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 업데이트\n"));
|
|
3184
|
-
// 패키지 데이터 초기화 (DB에서 조회)
|
|
3185
|
-
await initPackageData();
|
|
3186
2066
|
const cwd = process.cwd();
|
|
3187
2067
|
const semoSystemDir = path.join(cwd, "semo-system");
|
|
3188
2068
|
const claudeDir = path.join(cwd, ".claude");
|
|
@@ -3233,22 +2113,11 @@ program
|
|
|
3233
2113
|
console.log(chalk_1.default.red("SEMO가 설치되어 있지 않습니다. 'semo init'을 먼저 실행하세요."));
|
|
3234
2114
|
process.exit(1);
|
|
3235
2115
|
}
|
|
3236
|
-
// 설치된 Extensions 확인
|
|
3237
|
-
const installedExtensions = [];
|
|
3238
|
-
const extPkgs = getExtensionPackagesSync();
|
|
3239
|
-
for (const key of Object.keys(extPkgs)) {
|
|
3240
|
-
if (fs.existsSync(path.join(semoSystemDir, key))) {
|
|
3241
|
-
installedExtensions.push(key);
|
|
3242
|
-
}
|
|
3243
|
-
}
|
|
3244
2116
|
// 업데이트 대상 결정
|
|
3245
2117
|
const updateSemoCore = !isSelectiveUpdate || onlyPackages.includes("semo-core");
|
|
3246
2118
|
const updateSemoSkills = !isSelectiveUpdate || onlyPackages.includes("semo-skills");
|
|
3247
2119
|
const updateSemoAgents = !isSelectiveUpdate || onlyPackages.includes("semo-agents");
|
|
3248
2120
|
const updateSemoScripts = !isSelectiveUpdate || onlyPackages.includes("semo-scripts");
|
|
3249
|
-
const extensionsToUpdate = isSelectiveUpdate
|
|
3250
|
-
? installedExtensions.filter(ext => onlyPackages.includes(ext))
|
|
3251
|
-
: installedExtensions;
|
|
3252
2121
|
console.log(chalk_1.default.cyan("\n📚 semo-system 업데이트"));
|
|
3253
2122
|
console.log(chalk_1.default.gray(" 대상:"));
|
|
3254
2123
|
if (updateSemoCore)
|
|
@@ -3259,13 +2128,9 @@ program
|
|
|
3259
2128
|
console.log(chalk_1.default.gray(" - semo-agents"));
|
|
3260
2129
|
if (updateSemoScripts)
|
|
3261
2130
|
console.log(chalk_1.default.gray(" - semo-scripts"));
|
|
3262
|
-
|
|
3263
|
-
console.log(chalk_1.default.gray(` - ${pkg}`));
|
|
3264
|
-
});
|
|
3265
|
-
if (!updateSemoCore && !updateSemoSkills && !updateSemoAgents && !updateSemoScripts && extensionsToUpdate.length === 0) {
|
|
2131
|
+
if (!updateSemoCore && !updateSemoSkills && !updateSemoAgents && !updateSemoScripts) {
|
|
3266
2132
|
console.log(chalk_1.default.yellow("\n ⚠️ 업데이트할 패키지가 없습니다."));
|
|
3267
|
-
console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-skills, semo-agents, semo-scripts"
|
|
3268
|
-
(installedExtensions.length > 0 ? ", " + installedExtensions.join(", ") : "")));
|
|
2133
|
+
console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-skills, semo-agents, semo-scripts"));
|
|
3269
2134
|
return;
|
|
3270
2135
|
}
|
|
3271
2136
|
const spinner = (0, ora_1.default)("\n 최신 버전 다운로드 중...").start();
|
|
@@ -3290,15 +2155,6 @@ program
|
|
|
3290
2155
|
}
|
|
3291
2156
|
}
|
|
3292
2157
|
}
|
|
3293
|
-
// Extensions 업데이트 (선택적)
|
|
3294
|
-
for (const pkg of extensionsToUpdate) {
|
|
3295
|
-
const srcPath = path.join(tempDir, "packages", pkg);
|
|
3296
|
-
const destPath = path.join(semoSystemDir, pkg);
|
|
3297
|
-
if (fs.existsSync(srcPath)) {
|
|
3298
|
-
removeRecursive(destPath);
|
|
3299
|
-
copyRecursive(srcPath, destPath);
|
|
3300
|
-
}
|
|
3301
|
-
}
|
|
3302
2158
|
removeRecursive(tempDir);
|
|
3303
2159
|
spinner.succeed(" semo-system 업데이트 완료");
|
|
3304
2160
|
}
|
|
@@ -3343,40 +2199,13 @@ program
|
|
|
3343
2199
|
}
|
|
3344
2200
|
// Standard 심볼릭 링크 재생성 (agents, skills, commands 포함)
|
|
3345
2201
|
await createStandardSymlinks(cwd);
|
|
3346
|
-
// Extensions 심볼릭 링크 재생성
|
|
3347
|
-
if (installedExtensions.length > 0) {
|
|
3348
|
-
await setupExtensionSymlinks(cwd, installedExtensions);
|
|
3349
|
-
}
|
|
3350
2202
|
// === 4. CLAUDE.md 재생성 ===
|
|
3351
2203
|
console.log(chalk_1.default.cyan("\n📄 CLAUDE.md 재생성"));
|
|
3352
|
-
await setupClaudeMd(cwd,
|
|
2204
|
+
await setupClaudeMd(cwd, [], true);
|
|
3353
2205
|
// === 5. MCP 서버 동기화 ===
|
|
3354
2206
|
console.log(chalk_1.default.cyan("\n🔧 MCP 서버 동기화"));
|
|
3355
|
-
// Extension의 MCP 설정 확인 및 병합
|
|
3356
|
-
const allServers = [...BASE_MCP_SERVERS];
|
|
3357
|
-
for (const pkg of installedExtensions) {
|
|
3358
|
-
const extSettingsPath = path.join(semoSystemDir, pkg, "settings.local.json");
|
|
3359
|
-
if (fs.existsSync(extSettingsPath)) {
|
|
3360
|
-
try {
|
|
3361
|
-
const extSettings = JSON.parse(fs.readFileSync(extSettingsPath, "utf-8"));
|
|
3362
|
-
if (extSettings.mcpServers) {
|
|
3363
|
-
for (const [name, config] of Object.entries(extSettings.mcpServers)) {
|
|
3364
|
-
const serverConfig = config;
|
|
3365
|
-
allServers.push({
|
|
3366
|
-
name,
|
|
3367
|
-
command: serverConfig.command,
|
|
3368
|
-
args: serverConfig.args,
|
|
3369
|
-
env: serverConfig.env,
|
|
3370
|
-
});
|
|
3371
|
-
}
|
|
3372
|
-
}
|
|
3373
|
-
}
|
|
3374
|
-
catch {
|
|
3375
|
-
// 파싱 실패 무시
|
|
3376
|
-
}
|
|
3377
|
-
}
|
|
3378
|
-
}
|
|
3379
2207
|
// MCP 서버 등록 상태 확인
|
|
2208
|
+
const allServers = [...BASE_MCP_SERVERS];
|
|
3380
2209
|
const missingServers = [];
|
|
3381
2210
|
for (const server of allServers) {
|
|
3382
2211
|
if (!isMCPServerRegistered(server.name)) {
|
|
@@ -3401,7 +2230,7 @@ program
|
|
|
3401
2230
|
// === 6. Hooks 업데이트 ===
|
|
3402
2231
|
await setupHooks(cwd, true);
|
|
3403
2232
|
// === 7. 설치 검증 ===
|
|
3404
|
-
const verificationResult = verifyInstallation(cwd,
|
|
2233
|
+
const verificationResult = verifyInstallation(cwd, []);
|
|
3405
2234
|
printVerificationResult(verificationResult);
|
|
3406
2235
|
if (verificationResult.success) {
|
|
3407
2236
|
console.log(chalk_1.default.green.bold("\n✅ SEMO 업데이트 완료!\n"));
|
|
@@ -3547,6 +2376,629 @@ program
|
|
|
3547
2376
|
}
|
|
3548
2377
|
console.log();
|
|
3549
2378
|
});
|
|
2379
|
+
// === KB (Knowledge Base) 관리 ===
|
|
2380
|
+
const kb_1 = require("./kb");
|
|
2381
|
+
// Re-implement readSyncState locally (simple file read)
|
|
2382
|
+
function readSyncState(cwd) {
|
|
2383
|
+
const statePath = path.join(cwd, ".kb", ".sync-state.json");
|
|
2384
|
+
if (fs.existsSync(statePath)) {
|
|
2385
|
+
try {
|
|
2386
|
+
return JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
2387
|
+
}
|
|
2388
|
+
catch { /* */ }
|
|
2389
|
+
}
|
|
2390
|
+
return { botId: "", lastPull: null, lastPush: null, sharedCount: 0, botCount: 0 };
|
|
2391
|
+
}
|
|
2392
|
+
function detectBotId() {
|
|
2393
|
+
// 1. Env var
|
|
2394
|
+
if (process.env.SEMO_BOT_ID)
|
|
2395
|
+
return process.env.SEMO_BOT_ID;
|
|
2396
|
+
// 2. Detect from cwd (e.g. ~/.openclaw-workclaw → workclaw)
|
|
2397
|
+
const cwd = process.cwd();
|
|
2398
|
+
const match = cwd.match(/\.openclaw-(\w+)/);
|
|
2399
|
+
if (match)
|
|
2400
|
+
return match[1];
|
|
2401
|
+
// 3. Default
|
|
2402
|
+
return "unknown";
|
|
2403
|
+
}
|
|
2404
|
+
const kbCmd = program
|
|
2405
|
+
.command("kb")
|
|
2406
|
+
.description("KB(Knowledge Base) 관리 — SEMO DB 기반 지식 저장소");
|
|
2407
|
+
kbCmd
|
|
2408
|
+
.command("pull")
|
|
2409
|
+
.description("DB에서 KB를 로컬 .kb/로 내려받기")
|
|
2410
|
+
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2411
|
+
.option("--domain <name>", "특정 도메인만")
|
|
2412
|
+
.action(async (options) => {
|
|
2413
|
+
const spinner = (0, ora_1.default)("KB 데이터 가져오는 중...").start();
|
|
2414
|
+
try {
|
|
2415
|
+
const pool = (0, database_1.getPool)();
|
|
2416
|
+
const result = await (0, kb_1.kbPull)(pool, options.bot, options.domain, process.cwd());
|
|
2417
|
+
spinner.succeed(`KB pull 완료`);
|
|
2418
|
+
console.log(chalk_1.default.green(` 📦 공통 KB: ${result.shared.length}건`));
|
|
2419
|
+
console.log(chalk_1.default.green(` 🤖 봇 KB (${options.bot}): ${result.bot.length}건`));
|
|
2420
|
+
// Also pull ontology
|
|
2421
|
+
const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
|
|
2422
|
+
console.log(chalk_1.default.green(` 📐 온톨로지: ${ontoCount}개 도메인`));
|
|
2423
|
+
console.log(chalk_1.default.gray(`\n 저장 위치: .kb/`));
|
|
2424
|
+
await (0, database_1.closeConnection)();
|
|
2425
|
+
}
|
|
2426
|
+
catch (err) {
|
|
2427
|
+
spinner.fail(`KB pull 실패: ${err}`);
|
|
2428
|
+
await (0, database_1.closeConnection)();
|
|
2429
|
+
process.exit(1);
|
|
2430
|
+
}
|
|
2431
|
+
});
|
|
2432
|
+
kbCmd
|
|
2433
|
+
.command("push")
|
|
2434
|
+
.description("로컬 .kb/ 데이터를 DB에 업로드")
|
|
2435
|
+
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2436
|
+
.option("--target <type>", "대상 (shared|bot)", "bot")
|
|
2437
|
+
.option("--file <path>", ".kb/ 내 특정 파일")
|
|
2438
|
+
.action(async (options) => {
|
|
2439
|
+
const cwd = process.cwd();
|
|
2440
|
+
const kbDir = path.join(cwd, ".kb");
|
|
2441
|
+
if (!fs.existsSync(kbDir)) {
|
|
2442
|
+
console.log(chalk_1.default.red("❌ .kb/ 디렉토리가 없습니다. 먼저 semo kb pull을 실행하세요."));
|
|
2443
|
+
process.exit(1);
|
|
2444
|
+
}
|
|
2445
|
+
const spinner = (0, ora_1.default)("KB 데이터 업로드 중...").start();
|
|
2446
|
+
try {
|
|
2447
|
+
const pool = (0, database_1.getPool)();
|
|
2448
|
+
const filename = options.file || (options.target === "shared" ? "team.json" : "bot.json");
|
|
2449
|
+
const filePath = path.join(kbDir, filename);
|
|
2450
|
+
if (!fs.existsSync(filePath)) {
|
|
2451
|
+
spinner.fail(`파일을 찾을 수 없습니다: .kb/${filename}`);
|
|
2452
|
+
process.exit(1);
|
|
2453
|
+
}
|
|
2454
|
+
const entries = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2455
|
+
const result = await (0, kb_1.kbPush)(pool, options.bot, entries, options.target, cwd);
|
|
2456
|
+
spinner.succeed(`KB push 완료`);
|
|
2457
|
+
console.log(chalk_1.default.green(` ✅ ${result.upserted}건 업서트됨`));
|
|
2458
|
+
if (result.errors.length > 0) {
|
|
2459
|
+
console.log(chalk_1.default.yellow(` ⚠️ ${result.errors.length}건 오류:`));
|
|
2460
|
+
result.errors.forEach(e => console.log(chalk_1.default.red(` ${e}`)));
|
|
2461
|
+
}
|
|
2462
|
+
await (0, database_1.closeConnection)();
|
|
2463
|
+
}
|
|
2464
|
+
catch (err) {
|
|
2465
|
+
spinner.fail(`KB push 실패: ${err}`);
|
|
2466
|
+
await (0, database_1.closeConnection)();
|
|
2467
|
+
process.exit(1);
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
kbCmd
|
|
2471
|
+
.command("status")
|
|
2472
|
+
.description("KB 동기화 상태 확인")
|
|
2473
|
+
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2474
|
+
.action(async (options) => {
|
|
2475
|
+
const spinner = (0, ora_1.default)("KB 상태 조회 중...").start();
|
|
2476
|
+
try {
|
|
2477
|
+
const pool = (0, database_1.getPool)();
|
|
2478
|
+
const status = await (0, kb_1.kbStatus)(pool, options.bot);
|
|
2479
|
+
spinner.stop();
|
|
2480
|
+
console.log(chalk_1.default.cyan.bold("\n📊 KB 상태\n"));
|
|
2481
|
+
console.log(chalk_1.default.white(" 📦 공통 KB (shared)"));
|
|
2482
|
+
console.log(chalk_1.default.gray(` 총 ${status.shared.total}건`));
|
|
2483
|
+
for (const [domain, count] of Object.entries(status.shared.domains)) {
|
|
2484
|
+
console.log(chalk_1.default.gray(` - ${domain}: ${count}건`));
|
|
2485
|
+
}
|
|
2486
|
+
if (status.shared.lastUpdated) {
|
|
2487
|
+
console.log(chalk_1.default.gray(` 최종 업데이트: ${status.shared.lastUpdated}`));
|
|
2488
|
+
}
|
|
2489
|
+
console.log(chalk_1.default.white(`\n 🤖 봇 KB (${options.bot})`));
|
|
2490
|
+
console.log(chalk_1.default.gray(` 총 ${status.bot.total}건`));
|
|
2491
|
+
for (const [domain, count] of Object.entries(status.bot.domains)) {
|
|
2492
|
+
console.log(chalk_1.default.gray(` - ${domain}: ${count}건`));
|
|
2493
|
+
}
|
|
2494
|
+
if (status.bot.lastUpdated) {
|
|
2495
|
+
console.log(chalk_1.default.gray(` 최종 업데이트: ${status.bot.lastUpdated}`));
|
|
2496
|
+
}
|
|
2497
|
+
if (status.bot.lastSynced) {
|
|
2498
|
+
console.log(chalk_1.default.gray(` 최종 동기화: ${status.bot.lastSynced}`));
|
|
2499
|
+
}
|
|
2500
|
+
// Local sync state
|
|
2501
|
+
const syncState = readSyncState(process.cwd());
|
|
2502
|
+
if (syncState.lastPull || syncState.lastPush) {
|
|
2503
|
+
console.log(chalk_1.default.white("\n 🔄 로컬 동기화"));
|
|
2504
|
+
if (syncState.lastPull)
|
|
2505
|
+
console.log(chalk_1.default.gray(` 마지막 pull: ${syncState.lastPull}`));
|
|
2506
|
+
if (syncState.lastPush)
|
|
2507
|
+
console.log(chalk_1.default.gray(` 마지막 push: ${syncState.lastPush}`));
|
|
2508
|
+
}
|
|
2509
|
+
console.log();
|
|
2510
|
+
await (0, database_1.closeConnection)();
|
|
2511
|
+
}
|
|
2512
|
+
catch (err) {
|
|
2513
|
+
spinner.fail(`상태 조회 실패: ${err}`);
|
|
2514
|
+
await (0, database_1.closeConnection)();
|
|
2515
|
+
process.exit(1);
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
kbCmd
|
|
2519
|
+
.command("list")
|
|
2520
|
+
.description("KB 항목 목록 조회")
|
|
2521
|
+
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2522
|
+
.option("--domain <name>", "도메인 필터")
|
|
2523
|
+
.option("--limit <n>", "최대 항목 수", "50")
|
|
2524
|
+
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
2525
|
+
.action(async (options) => {
|
|
2526
|
+
try {
|
|
2527
|
+
const pool = (0, database_1.getPool)();
|
|
2528
|
+
const result = await (0, kb_1.kbList)(pool, {
|
|
2529
|
+
domain: options.domain,
|
|
2530
|
+
botId: options.bot,
|
|
2531
|
+
limit: parseInt(options.limit),
|
|
2532
|
+
});
|
|
2533
|
+
if (options.format === "json") {
|
|
2534
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2535
|
+
}
|
|
2536
|
+
else {
|
|
2537
|
+
console.log(chalk_1.default.cyan.bold("\n📋 KB 목록\n"));
|
|
2538
|
+
if (result.shared.length > 0) {
|
|
2539
|
+
console.log(chalk_1.default.white(" 📦 공통 KB"));
|
|
2540
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
2541
|
+
for (const entry of result.shared) {
|
|
2542
|
+
const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
|
|
2543
|
+
console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
|
|
2544
|
+
console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
if (result.bot.length > 0) {
|
|
2548
|
+
console.log(chalk_1.default.white(`\n 🤖 봇 KB (${options.bot})`));
|
|
2549
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
2550
|
+
for (const entry of result.bot) {
|
|
2551
|
+
const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
|
|
2552
|
+
console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
|
|
2553
|
+
console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
if (result.shared.length === 0 && result.bot.length === 0) {
|
|
2557
|
+
console.log(chalk_1.default.yellow(" KB가 비어있습니다."));
|
|
2558
|
+
}
|
|
2559
|
+
console.log();
|
|
2560
|
+
}
|
|
2561
|
+
await (0, database_1.closeConnection)();
|
|
2562
|
+
}
|
|
2563
|
+
catch (err) {
|
|
2564
|
+
console.error(chalk_1.default.red(`조회 실패: ${err}`));
|
|
2565
|
+
await (0, database_1.closeConnection)();
|
|
2566
|
+
process.exit(1);
|
|
2567
|
+
}
|
|
2568
|
+
});
|
|
2569
|
+
kbCmd
|
|
2570
|
+
.command("search <query>")
|
|
2571
|
+
.description("KB 검색 (시맨틱 + 텍스트 하이브리드)")
|
|
2572
|
+
.option("--bot <name>", "봇 KB도 검색", detectBotId())
|
|
2573
|
+
.option("--domain <name>", "도메인 필터")
|
|
2574
|
+
.option("--limit <n>", "최대 결과 수", "10")
|
|
2575
|
+
.option("--mode <type>", "검색 모드 (hybrid|semantic|text)", "hybrid")
|
|
2576
|
+
.action(async (query, options) => {
|
|
2577
|
+
const spinner = (0, ora_1.default)(`'${query}' 검색 중...`).start();
|
|
2578
|
+
try {
|
|
2579
|
+
const pool = (0, database_1.getPool)();
|
|
2580
|
+
const results = await (0, kb_1.kbSearch)(pool, query, {
|
|
2581
|
+
domain: options.domain,
|
|
2582
|
+
botId: options.bot,
|
|
2583
|
+
limit: parseInt(options.limit),
|
|
2584
|
+
mode: options.mode,
|
|
2585
|
+
});
|
|
2586
|
+
spinner.stop();
|
|
2587
|
+
if (results.length === 0) {
|
|
2588
|
+
console.log(chalk_1.default.yellow(`\n 검색 결과 없음: '${query}'`));
|
|
2589
|
+
}
|
|
2590
|
+
else {
|
|
2591
|
+
console.log(chalk_1.default.cyan.bold(`\n🔍 검색 결과: '${query}' (${results.length}건)\n`));
|
|
2592
|
+
for (const entry of results) {
|
|
2593
|
+
const preview = entry.content.substring(0, 80).replace(/\n/g, " ");
|
|
2594
|
+
const score = entry.score;
|
|
2595
|
+
const scoreStr = score ? chalk_1.default.yellow(` (${(score * 100).toFixed(1)}%)`) : "";
|
|
2596
|
+
console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key) + scoreStr);
|
|
2597
|
+
console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 80 ? "..." : ""}`));
|
|
2598
|
+
console.log();
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
await (0, database_1.closeConnection)();
|
|
2602
|
+
}
|
|
2603
|
+
catch (err) {
|
|
2604
|
+
spinner.fail(`검색 실패: ${err}`);
|
|
2605
|
+
await (0, database_1.closeConnection)();
|
|
2606
|
+
process.exit(1);
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
kbCmd
|
|
2610
|
+
.command("diff")
|
|
2611
|
+
.description("로컬 .kb/ vs DB 차이 비교")
|
|
2612
|
+
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2613
|
+
.action(async (options) => {
|
|
2614
|
+
const spinner = (0, ora_1.default)("차이 비교 중...").start();
|
|
2615
|
+
try {
|
|
2616
|
+
const pool = (0, database_1.getPool)();
|
|
2617
|
+
const diff = await (0, kb_1.kbDiff)(pool, options.bot, process.cwd());
|
|
2618
|
+
spinner.stop();
|
|
2619
|
+
console.log(chalk_1.default.cyan.bold("\n📊 KB Diff\n"));
|
|
2620
|
+
console.log(chalk_1.default.green(` ✚ 추가됨 (DB에만 있음): ${diff.added.length}건`));
|
|
2621
|
+
console.log(chalk_1.default.red(` ✖ 삭제됨 (로컬에만 있음): ${diff.removed.length}건`));
|
|
2622
|
+
console.log(chalk_1.default.yellow(` ✎ 변경됨: ${diff.modified.length}건`));
|
|
2623
|
+
console.log(chalk_1.default.gray(` ═ 동일: ${diff.unchanged}건`));
|
|
2624
|
+
if (diff.added.length > 0) {
|
|
2625
|
+
console.log(chalk_1.default.green("\n ✚ 추가된 항목:"));
|
|
2626
|
+
diff.added.forEach(e => console.log(chalk_1.default.gray(` [${e.domain}] ${e.key}`)));
|
|
2627
|
+
}
|
|
2628
|
+
if (diff.removed.length > 0) {
|
|
2629
|
+
console.log(chalk_1.default.red("\n ✖ 삭제된 항목:"));
|
|
2630
|
+
diff.removed.forEach(e => console.log(chalk_1.default.gray(` [${e.domain}] ${e.key}`)));
|
|
2631
|
+
}
|
|
2632
|
+
if (diff.modified.length > 0) {
|
|
2633
|
+
console.log(chalk_1.default.yellow("\n ✎ 변경된 항목:"));
|
|
2634
|
+
diff.modified.forEach(m => console.log(chalk_1.default.gray(` [${m.local.domain}] ${m.local.key}`)));
|
|
2635
|
+
}
|
|
2636
|
+
console.log();
|
|
2637
|
+
await (0, database_1.closeConnection)();
|
|
2638
|
+
}
|
|
2639
|
+
catch (err) {
|
|
2640
|
+
spinner.fail(`Diff 실패: ${err}`);
|
|
2641
|
+
await (0, database_1.closeConnection)();
|
|
2642
|
+
process.exit(1);
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
kbCmd
|
|
2646
|
+
.command("embed")
|
|
2647
|
+
.description("기존 KB 항목에 임베딩 벡터 생성 (VOYAGE_API_KEY 필요)")
|
|
2648
|
+
.option("--bot <name>", "봇 KB도 임베딩", detectBotId())
|
|
2649
|
+
.option("--domain <name>", "도메인 필터")
|
|
2650
|
+
.option("--force", "이미 임베딩된 항목도 재생성")
|
|
2651
|
+
.action(async (options) => {
|
|
2652
|
+
if (!process.env.VOYAGE_API_KEY) {
|
|
2653
|
+
console.log(chalk_1.default.red("❌ VOYAGE_API_KEY 환경변수가 설정되지 않았습니다."));
|
|
2654
|
+
console.log(chalk_1.default.gray(" export VOYAGE_API_KEY='pa-...'"));
|
|
2655
|
+
process.exit(1);
|
|
2656
|
+
}
|
|
2657
|
+
const spinner = (0, ora_1.default)("임베딩 대상 조회 중...").start();
|
|
2658
|
+
try {
|
|
2659
|
+
const pool = (0, database_1.getPool)();
|
|
2660
|
+
const client = await pool.connect();
|
|
2661
|
+
// Shared KB
|
|
2662
|
+
let sharedSql = "SELECT kb_id, domain, key, content FROM semo.knowledge_base WHERE 1=1";
|
|
2663
|
+
const sharedParams = [];
|
|
2664
|
+
let pIdx = 1;
|
|
2665
|
+
if (!options.force)
|
|
2666
|
+
sharedSql += " AND embedding IS NULL";
|
|
2667
|
+
if (options.domain) {
|
|
2668
|
+
sharedSql += ` AND domain = $${pIdx++}`;
|
|
2669
|
+
sharedParams.push(options.domain);
|
|
2670
|
+
}
|
|
2671
|
+
const sharedRows = await client.query(sharedSql, sharedParams);
|
|
2672
|
+
// Bot KB
|
|
2673
|
+
let botSql = "SELECT id, domain, key, content FROM semo.bot_knowledge WHERE bot_id = $1";
|
|
2674
|
+
const botParams = [options.bot];
|
|
2675
|
+
let bIdx = 2;
|
|
2676
|
+
if (!options.force)
|
|
2677
|
+
botSql += " AND embedding IS NULL";
|
|
2678
|
+
if (options.domain) {
|
|
2679
|
+
botSql += ` AND domain = $${bIdx++}`;
|
|
2680
|
+
botParams.push(options.domain);
|
|
2681
|
+
}
|
|
2682
|
+
const botRows = await client.query(botSql, botParams);
|
|
2683
|
+
const total = sharedRows.rows.length + botRows.rows.length;
|
|
2684
|
+
spinner.succeed(`${total}건 임베딩 대상`);
|
|
2685
|
+
if (total === 0) {
|
|
2686
|
+
console.log(chalk_1.default.green(" 모든 항목이 이미 임베딩되어 있습니다."));
|
|
2687
|
+
client.release();
|
|
2688
|
+
await (0, database_1.closeConnection)();
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
let done = 0;
|
|
2692
|
+
const embedSpinner = (0, ora_1.default)(`임베딩 생성 중... 0/${total}`).start();
|
|
2693
|
+
// Process shared KB
|
|
2694
|
+
for (const row of sharedRows.rows) {
|
|
2695
|
+
const embedding = await (0, kb_1.generateEmbedding)(`${row.key}: ${row.content}`);
|
|
2696
|
+
if (embedding) {
|
|
2697
|
+
await client.query("UPDATE semo.knowledge_base SET embedding = $1::vector WHERE kb_id = $2", [`[${embedding.join(",")}]`, row.kb_id]);
|
|
2698
|
+
}
|
|
2699
|
+
done++;
|
|
2700
|
+
embedSpinner.text = `임베딩 생성 중... ${done}/${total}`;
|
|
2701
|
+
}
|
|
2702
|
+
// Process bot KB
|
|
2703
|
+
for (const row of botRows.rows) {
|
|
2704
|
+
const embedding = await (0, kb_1.generateEmbedding)(`${row.key}: ${row.content}`);
|
|
2705
|
+
if (embedding) {
|
|
2706
|
+
await client.query("UPDATE semo.bot_knowledge SET embedding = $1::vector WHERE id = $2", [`[${embedding.join(",")}]`, row.id]);
|
|
2707
|
+
}
|
|
2708
|
+
done++;
|
|
2709
|
+
embedSpinner.text = `임베딩 생성 중... ${done}/${total}`;
|
|
2710
|
+
}
|
|
2711
|
+
embedSpinner.succeed(`${done}건 임베딩 완료`);
|
|
2712
|
+
client.release();
|
|
2713
|
+
await (0, database_1.closeConnection)();
|
|
2714
|
+
}
|
|
2715
|
+
catch (err) {
|
|
2716
|
+
spinner.fail(`임베딩 실패: ${err}`);
|
|
2717
|
+
await (0, database_1.closeConnection)();
|
|
2718
|
+
process.exit(1);
|
|
2719
|
+
}
|
|
2720
|
+
});
|
|
2721
|
+
kbCmd
|
|
2722
|
+
.command("sync")
|
|
2723
|
+
.description("양방향 동기화 (pull → merge → push)")
|
|
2724
|
+
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2725
|
+
.option("--domain <name>", "도메인 필터")
|
|
2726
|
+
.action(async (options) => {
|
|
2727
|
+
console.log(chalk_1.default.cyan.bold("\n🔄 KB 동기화\n"));
|
|
2728
|
+
const spinner = (0, ora_1.default)("Step 1/2: DB에서 pull...").start();
|
|
2729
|
+
try {
|
|
2730
|
+
const pool = (0, database_1.getPool)();
|
|
2731
|
+
// Step 1: Pull
|
|
2732
|
+
const pulled = await (0, kb_1.kbPull)(pool, options.bot, options.domain, process.cwd());
|
|
2733
|
+
spinner.succeed(`Pull 완료: 공통 ${pulled.shared.length}건, 봇 ${pulled.bot.length}건`);
|
|
2734
|
+
// Step 2: Pull ontology
|
|
2735
|
+
const spinner2 = (0, ora_1.default)("Step 2/2: 온톨로지 동기화...").start();
|
|
2736
|
+
const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
|
|
2737
|
+
spinner2.succeed(`온톨로지 ${ontoCount}개 도메인 동기화됨`);
|
|
2738
|
+
console.log(chalk_1.default.green.bold("\n✅ 동기화 완료\n"));
|
|
2739
|
+
console.log(chalk_1.default.gray(" 로컬 수정 후 semo kb push로 업로드하세요."));
|
|
2740
|
+
console.log();
|
|
2741
|
+
await (0, database_1.closeConnection)();
|
|
2742
|
+
}
|
|
2743
|
+
catch (err) {
|
|
2744
|
+
spinner.fail(`동기화 실패: ${err}`);
|
|
2745
|
+
await (0, database_1.closeConnection)();
|
|
2746
|
+
process.exit(1);
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
2749
|
+
// === Ontology 관리 ===
|
|
2750
|
+
const ontoCmd = program
|
|
2751
|
+
.command("onto")
|
|
2752
|
+
.description("온톨로지(Ontology) 관리 — 도메인 스키마 정의");
|
|
2753
|
+
ontoCmd
|
|
2754
|
+
.command("list")
|
|
2755
|
+
.description("정의된 온톨로지 도메인 목록")
|
|
2756
|
+
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
2757
|
+
.action(async (options) => {
|
|
2758
|
+
try {
|
|
2759
|
+
const pool = (0, database_1.getPool)();
|
|
2760
|
+
const domains = await (0, kb_1.ontoList)(pool);
|
|
2761
|
+
if (options.format === "json") {
|
|
2762
|
+
console.log(JSON.stringify(domains, null, 2));
|
|
2763
|
+
}
|
|
2764
|
+
else {
|
|
2765
|
+
console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 도메인\n"));
|
|
2766
|
+
if (domains.length === 0) {
|
|
2767
|
+
console.log(chalk_1.default.yellow(" 온톨로지가 정의되지 않았습니다."));
|
|
2768
|
+
}
|
|
2769
|
+
else {
|
|
2770
|
+
for (const d of domains) {
|
|
2771
|
+
console.log(chalk_1.default.cyan(` ${d.domain}`) + chalk_1.default.gray(` (v${d.version})`));
|
|
2772
|
+
if (d.description)
|
|
2773
|
+
console.log(chalk_1.default.gray(` ${d.description}`));
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
console.log();
|
|
2777
|
+
}
|
|
2778
|
+
await (0, database_1.closeConnection)();
|
|
2779
|
+
}
|
|
2780
|
+
catch (err) {
|
|
2781
|
+
console.error(chalk_1.default.red(`조회 실패: ${err}`));
|
|
2782
|
+
await (0, database_1.closeConnection)();
|
|
2783
|
+
process.exit(1);
|
|
2784
|
+
}
|
|
2785
|
+
});
|
|
2786
|
+
ontoCmd
|
|
2787
|
+
.command("show <domain>")
|
|
2788
|
+
.description("특정 도메인 온톨로지 상세")
|
|
2789
|
+
.action(async (domain) => {
|
|
2790
|
+
try {
|
|
2791
|
+
const pool = (0, database_1.getPool)();
|
|
2792
|
+
const onto = await (0, kb_1.ontoShow)(pool, domain);
|
|
2793
|
+
if (!onto) {
|
|
2794
|
+
console.log(chalk_1.default.red(`\n 온톨로지 '${domain}'을 찾을 수 없습니다.\n`));
|
|
2795
|
+
process.exit(1);
|
|
2796
|
+
}
|
|
2797
|
+
console.log(chalk_1.default.cyan.bold(`\n📐 온톨로지: ${onto.domain}\n`));
|
|
2798
|
+
if (onto.description)
|
|
2799
|
+
console.log(chalk_1.default.white(` ${onto.description}`));
|
|
2800
|
+
console.log(chalk_1.default.gray(` 버전: ${onto.version}`));
|
|
2801
|
+
console.log(chalk_1.default.gray(` 스키마:\n`));
|
|
2802
|
+
console.log(chalk_1.default.white(JSON.stringify(onto.schema, null, 2).split("\n").map(l => " " + l).join("\n")));
|
|
2803
|
+
console.log();
|
|
2804
|
+
await (0, database_1.closeConnection)();
|
|
2805
|
+
}
|
|
2806
|
+
catch (err) {
|
|
2807
|
+
console.error(chalk_1.default.red(`조회 실패: ${err}`));
|
|
2808
|
+
await (0, database_1.closeConnection)();
|
|
2809
|
+
process.exit(1);
|
|
2810
|
+
}
|
|
2811
|
+
});
|
|
2812
|
+
ontoCmd
|
|
2813
|
+
.command("validate [domain]")
|
|
2814
|
+
.description("KB 항목이 온톨로지 스키마와 일치하는지 검증")
|
|
2815
|
+
.option("--all", "모든 도메인 검증")
|
|
2816
|
+
.action(async (domain, options) => {
|
|
2817
|
+
try {
|
|
2818
|
+
const pool = (0, database_1.getPool)();
|
|
2819
|
+
const domainsToCheck = [];
|
|
2820
|
+
if (options.all) {
|
|
2821
|
+
const allDomains = await (0, kb_1.ontoList)(pool);
|
|
2822
|
+
domainsToCheck.push(...allDomains.map(d => d.domain));
|
|
2823
|
+
}
|
|
2824
|
+
else if (domain) {
|
|
2825
|
+
domainsToCheck.push(domain);
|
|
2826
|
+
}
|
|
2827
|
+
else {
|
|
2828
|
+
console.log(chalk_1.default.red("도메인을 지정하거나 --all 옵션을 사용하세요."));
|
|
2829
|
+
process.exit(1);
|
|
2830
|
+
}
|
|
2831
|
+
console.log(chalk_1.default.cyan.bold("\n🔍 온톨로지 검증\n"));
|
|
2832
|
+
let totalValid = 0;
|
|
2833
|
+
let totalInvalid = 0;
|
|
2834
|
+
for (const d of domainsToCheck) {
|
|
2835
|
+
const result = await (0, kb_1.ontoValidate)(pool, d);
|
|
2836
|
+
totalValid += result.valid;
|
|
2837
|
+
totalInvalid += result.invalid.length;
|
|
2838
|
+
if (result.invalid.length === 0) {
|
|
2839
|
+
console.log(chalk_1.default.green(` ✅ ${d}: ${result.valid}건 모두 유효`));
|
|
2840
|
+
}
|
|
2841
|
+
else {
|
|
2842
|
+
console.log(chalk_1.default.yellow(` ⚠️ ${d}: ${result.valid}건 유효, ${result.invalid.length}건 오류`));
|
|
2843
|
+
for (const inv of result.invalid) {
|
|
2844
|
+
console.log(chalk_1.default.red(` ${inv.key}:`));
|
|
2845
|
+
inv.errors.forEach(e => console.log(chalk_1.default.gray(` - ${e}`)));
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
console.log(chalk_1.default.gray(`\n 총 결과: ${totalValid}건 유효, ${totalInvalid}건 오류\n`));
|
|
2850
|
+
await (0, database_1.closeConnection)();
|
|
2851
|
+
}
|
|
2852
|
+
catch (err) {
|
|
2853
|
+
console.error(chalk_1.default.red(`검증 실패: ${err}`));
|
|
2854
|
+
await (0, database_1.closeConnection)();
|
|
2855
|
+
process.exit(1);
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
// === 신규 v4 커맨드 그룹 등록 ===
|
|
2859
|
+
(0, context_1.registerContextCommands)(program);
|
|
2860
|
+
(0, bots_1.registerBotsCommands)(program);
|
|
2861
|
+
(0, get_1.registerGetCommands)(program);
|
|
2862
|
+
// === semo skills — DB 시딩 ===
|
|
2863
|
+
/**
|
|
2864
|
+
* SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
|
|
2865
|
+
*/
|
|
2866
|
+
function parseSkillFrontmatter(content) {
|
|
2867
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2868
|
+
if (!fmMatch)
|
|
2869
|
+
return null;
|
|
2870
|
+
const fm = fmMatch[1];
|
|
2871
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
2872
|
+
const name = nameMatch ? nameMatch[1].trim() : "";
|
|
2873
|
+
if (!name)
|
|
2874
|
+
return null;
|
|
2875
|
+
// multi-line description (| block scalar)
|
|
2876
|
+
let description = "";
|
|
2877
|
+
const descBlockMatch = fm.match(/^description:\s*\|\n([\s\S]*?)(?=^[a-z]|\n---)/m);
|
|
2878
|
+
if (descBlockMatch) {
|
|
2879
|
+
description = descBlockMatch[1].replace(/^ /gm, "").trim();
|
|
2880
|
+
}
|
|
2881
|
+
else {
|
|
2882
|
+
const descInlineMatch = fm.match(/^description:\s*(.+)$/m);
|
|
2883
|
+
if (descInlineMatch)
|
|
2884
|
+
description = descInlineMatch[1].trim();
|
|
2885
|
+
}
|
|
2886
|
+
// tools 배열
|
|
2887
|
+
const toolsMatch = fm.match(/^tools:\s*\[(.+)\]$/m);
|
|
2888
|
+
const tools = toolsMatch ? toolsMatch[1].split(",").map((t) => t.trim()) : [];
|
|
2889
|
+
// category: 상위 디렉토리명 또는 name 자체 (semo-skills의 경우 dir = skill name)
|
|
2890
|
+
const category = "core";
|
|
2891
|
+
return { name, description, category, tools };
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* semo-system/semo-skills/ 스캔 → semo.skills 테이블 upsert
|
|
2895
|
+
*/
|
|
2896
|
+
async function seedSkillsToDb(semoSystemDir) {
|
|
2897
|
+
const skillsDir = path.join(semoSystemDir, "semo-skills");
|
|
2898
|
+
if (!fs.existsSync(skillsDir)) {
|
|
2899
|
+
console.log(chalk_1.default.red(`\n❌ semo-skills 디렉토리를 찾을 수 없습니다: ${skillsDir}`));
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
2903
|
+
if (!connected) {
|
|
2904
|
+
console.log(chalk_1.default.red("❌ DB 연결 실패 — seed-skills 건너뜀"));
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
const spinner = (0, ora_1.default)("semo-skills 스캔 중...").start();
|
|
2908
|
+
// 활성 스킬 디렉토리 수집 (_archived, CHANGELOG, VERSION 제외)
|
|
2909
|
+
const excludeDirs = new Set(["_archived", "CHANGELOG"]);
|
|
2910
|
+
const excludeFiles = new Set(["VERSION"]);
|
|
2911
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
2912
|
+
const skillDirs = entries.filter(e => e.isDirectory() && !excludeDirs.has(e.name));
|
|
2913
|
+
const skills = [];
|
|
2914
|
+
for (const dir of skillDirs) {
|
|
2915
|
+
const skillMdPath = path.join(skillsDir, dir.name, "SKILL.md");
|
|
2916
|
+
if (!fs.existsSync(skillMdPath))
|
|
2917
|
+
continue;
|
|
2918
|
+
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
2919
|
+
const parsed = parseSkillFrontmatter(content);
|
|
2920
|
+
if (!parsed)
|
|
2921
|
+
continue;
|
|
2922
|
+
skills.push({ ...parsed, content });
|
|
2923
|
+
}
|
|
2924
|
+
spinner.text = `${skills.length}개 스킬 발견 — DB에 upsert 중...`;
|
|
2925
|
+
const pool = (0, database_1.getPool)();
|
|
2926
|
+
const client = await pool.connect();
|
|
2927
|
+
let upserted = 0;
|
|
2928
|
+
const errors = [];
|
|
2929
|
+
try {
|
|
2930
|
+
await client.query("BEGIN");
|
|
2931
|
+
for (let i = 0; i < skills.length; i++) {
|
|
2932
|
+
const skill = skills[i];
|
|
2933
|
+
try {
|
|
2934
|
+
await client.query(`INSERT INTO semo.skills
|
|
2935
|
+
(name, display_name, description, content, category, package,
|
|
2936
|
+
is_active, is_required, install_order, version)
|
|
2937
|
+
VALUES ($1, $2, $3, $4, $5, $6, true, false, $7, '1.0.0')
|
|
2938
|
+
ON CONFLICT (name) DO UPDATE SET
|
|
2939
|
+
display_name = EXCLUDED.display_name,
|
|
2940
|
+
description = EXCLUDED.description,
|
|
2941
|
+
content = EXCLUDED.content,
|
|
2942
|
+
category = EXCLUDED.category`, [skill.name, skill.name, skill.description, skill.content, skill.category, "semo-skills", i + 1]);
|
|
2943
|
+
upserted++;
|
|
2944
|
+
}
|
|
2945
|
+
catch (err) {
|
|
2946
|
+
errors.push(`${skill.name}: ${err}`);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
await client.query("COMMIT");
|
|
2950
|
+
spinner.succeed(`seed-skills 완료: ${upserted}개 스킬 upsert`);
|
|
2951
|
+
if (errors.length > 0) {
|
|
2952
|
+
errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
catch (err) {
|
|
2956
|
+
await client.query("ROLLBACK");
|
|
2957
|
+
spinner.fail(`seed-skills 실패: ${err}`);
|
|
2958
|
+
}
|
|
2959
|
+
finally {
|
|
2960
|
+
client.release();
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
// `semo skills` 커맨드 그룹
|
|
2964
|
+
const skillsCmd = program
|
|
2965
|
+
.command("skills")
|
|
2966
|
+
.description("스킬 관리 (DB 시딩 등)");
|
|
2967
|
+
skillsCmd
|
|
2968
|
+
.command("seed")
|
|
2969
|
+
.description("semo-system/semo-skills/ → semo.skills DB upsert")
|
|
2970
|
+
.option("--semo-system <path>", "semo-system 경로 (기본: ./semo-system)")
|
|
2971
|
+
.option("--dry-run", "실제 upsert 없이 스캔 결과만 출력")
|
|
2972
|
+
.action(async (options) => {
|
|
2973
|
+
const cwd = process.cwd();
|
|
2974
|
+
const semoSystemDir = options.semoSystem
|
|
2975
|
+
? path.resolve(options.semoSystem)
|
|
2976
|
+
: path.join(cwd, "semo-system");
|
|
2977
|
+
if (options.dryRun) {
|
|
2978
|
+
const skillsDir = path.join(semoSystemDir, "semo-skills");
|
|
2979
|
+
if (!fs.existsSync(skillsDir)) {
|
|
2980
|
+
console.log(chalk_1.default.red(`❌ ${skillsDir} 없음`));
|
|
2981
|
+
process.exit(1);
|
|
2982
|
+
}
|
|
2983
|
+
const excludeDirs = new Set(["_archived", "CHANGELOG"]);
|
|
2984
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
2985
|
+
const skillDirs = entries.filter(e => e.isDirectory() && !excludeDirs.has(e.name));
|
|
2986
|
+
console.log(chalk_1.default.cyan.bold("\n[dry-run] 발견된 스킬:\n"));
|
|
2987
|
+
for (const dir of skillDirs) {
|
|
2988
|
+
const mdPath = path.join(skillsDir, dir.name, "SKILL.md");
|
|
2989
|
+
if (!fs.existsSync(mdPath))
|
|
2990
|
+
continue;
|
|
2991
|
+
const parsed = parseSkillFrontmatter(fs.readFileSync(mdPath, "utf-8"));
|
|
2992
|
+
if (parsed) {
|
|
2993
|
+
console.log(chalk_1.default.gray(` ${parsed.name.padEnd(20)} ${parsed.description.split("\n")[0].substring(0, 60)}`));
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
console.log();
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
await seedSkillsToDb(semoSystemDir);
|
|
3000
|
+
await (0, database_1.closeConnection)();
|
|
3001
|
+
});
|
|
3550
3002
|
// === -v 옵션 처리 (program.parse 전에 직접 처리) ===
|
|
3551
3003
|
async function main() {
|
|
3552
3004
|
const args = process.argv.slice(2);
|