@team-semicolon/semo-cli 3.14.1 → 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/index.js CHANGED
@@ -1,18 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * SEMO CLI v2.0
4
+ * SEMO CLI v4.0
5
5
  *
6
- * Gemini 하이브리드 전략 기반 AI Agent Orchestration Framework
6
+ * Core DB 기반 컨텍스트 동기화 시스템
7
7
  *
8
8
  * 사용법:
9
- * npx @team-semicolon/semo-cli init # 기본 설치
10
- * npx @team-semicolon/semo-cli add next # 패키지 추가
11
- * npx @team-semicolon/semo-cli list # 패키지 목록
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/ 내부의 레거시 Extension 그룹 삭제
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. Extension 패키지 처리 (--with 옵션만 지원, 인터랙션 없음)
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
- // 4. Standard 설치 (semo-core + semo-skills)
811
+ // 3. Standard 설치 (semo-core + semo-skills)
1230
812
  await setupStandard(cwd, options.force);
1231
- // 5. Extensions 다운로드 (심볼릭 링크는 아직)
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, extensionsToInstall, options.force);
815
+ await setupMCP(cwd, [], options.force);
1238
816
  }
1239
- // 7. Context Mesh 초기화
817
+ // 5. Context Mesh 초기화
1240
818
  await setupContextMesh(cwd);
1241
- // 8. .gitignore 업데이트
819
+ // 6. .gitignore 업데이트
1242
820
  if (options.gitignore !== false) {
1243
821
  updateGitignore(cwd);
1244
822
  }
1245
- // 9. Hooks 설치 (대화 로깅)
823
+ // 7. Hooks 설치 (대화 로깅)
1246
824
  await setupHooks(cwd, false);
1247
- // 10. CLAUDE.md 생성
1248
- await setupClaudeMd(cwd, extensionsToInstall, options.force);
1249
- // 11. Extensions 심볼릭 링크 (agents/skills 병합)
1250
- if (extensionsToInstall.length > 0) {
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, extensions, force) {
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
- const content = fs.readFileSync(gitignorePath, "utf-8");
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.appendFileSync(gitignorePath, semoIgnoreBlock);
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
- // "Core Rules (상속)" 패턴을 사용하는 Extension은 고유 섹션만 추출
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 semoSystemDir = path.join(cwd, "semo-system");
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
- ├── semo-skills/ # Layer 1: 통합 스킬
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
- const extPkgs = getExtensionPackagesSync();
3070
- console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록 (v3.10 - DB 기반)\n"));
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 coreInstalled = fs.existsSync(path.join(semoSystemDir, "semo-core"));
3074
- const skillsInstalled = fs.existsSync(path.join(semoSystemDir, "semo-skills"));
3075
- console.log(` ${coreInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} semo-core - 원칙, 오케스트레이터`);
3076
- console.log(` ${skillsInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} semo-skills - 통합 스킬`);
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
- // Extensions - 레이어별 그룹화
3079
- const layers = {
3080
- biz: { title: "Business Layer", emoji: "💼" },
3081
- eng: { title: "Engineering Layer", emoji: "⚙️" },
3082
- ops: { title: "Operations Layer", emoji: "📊" },
3083
- meta: { title: "Meta", emoji: "🔧" },
3084
- system: { title: "System", emoji: "🔩" },
3085
- };
3086
- for (const [layerKey, layerInfo] of Object.entries(layers)) {
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
- console.log(chalk_1.default.gray("─".repeat(50)));
3102
- console.log(chalk_1.default.white.bold("📦 그룹 일괄 설치"));
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
- extensionsToUpdate.forEach(pkg => {
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, installedExtensions, true);
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, installedExtensions);
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);