@team-semicolon/semo-cli 4.1.5 → 4.3.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
@@ -56,11 +56,15 @@ const child_process_1 = require("child_process");
56
56
  const fs = __importStar(require("fs"));
57
57
  const path = __importStar(require("path"));
58
58
  const os = __importStar(require("os"));
59
+ const env_parser_1 = require("./env-parser");
59
60
  const database_1 = require("./database");
60
61
  const context_1 = require("./commands/context");
61
62
  const bots_1 = require("./commands/bots");
62
63
  const get_1 = require("./commands/get");
63
64
  const sessions_1 = require("./commands/sessions");
65
+ const db_1 = require("./commands/db");
66
+ const memory_1 = require("./commands/memory");
67
+ const global_cache_1 = require("./global-cache");
64
68
  const PACKAGE_NAME = "@team-semicolon/semo-cli";
65
69
  // package.json에서 버전 동적 로드
66
70
  function getCliVersion() {
@@ -133,7 +137,7 @@ async function getRemotePackageVersion(packagePath) {
133
137
  }
134
138
  }
135
139
  /**
136
- * semo-core/semo-skills 원격 버전 가져오기 (semo-system/ 하위 경로)
140
+ * semo-core 원격 버전 가져오기 (semo-system/ 하위 경로)
137
141
  */
138
142
  async function getRemoteCoreVersion(type) {
139
143
  try {
@@ -159,7 +163,7 @@ async function showVersionComparison(cwd) {
159
163
  // 1. CLI 버전 비교
160
164
  const currentCliVersion = VERSION;
161
165
  const latestCliVersion = await getLatestVersion();
162
- // 2. semo-core, semo-skills 버전 비교
166
+ // 2. semo-core 버전 비교
163
167
  const semoSystemDir = path.join(cwd, "semo-system");
164
168
  const hasSemoSystem = fs.existsSync(semoSystemDir);
165
169
  const versionInfos = [];
@@ -171,15 +175,14 @@ async function showVersionComparison(cwd) {
171
175
  needsUpdate: latestCliVersion ? isVersionLower(currentCliVersion, latestCliVersion) : false,
172
176
  level: 0,
173
177
  });
174
- // 레거시 환경 경고 (루트에 semo-core/semo-skills가 있는 경우)
178
+ // 레거시 환경 경고 (루트에 semo-core가 있는 경우)
175
179
  const hasLegacyCore = fs.existsSync(path.join(cwd, "semo-core"));
176
- const hasLegacySkills = fs.existsSync(path.join(cwd, "semo-skills"));
177
- if (hasLegacyCore || hasLegacySkills) {
180
+ if (hasLegacyCore) {
178
181
  spinner.warn("레거시 환경 감지됨");
179
182
  console.log(chalk_1.default.yellow("\n ⚠️ 구버전 SEMO 구조가 감지되었습니다."));
180
- console.log(chalk_1.default.gray(" 루트에 semo-core/ 또는 semo-skills/가 있습니다."));
183
+ console.log(chalk_1.default.gray(" 루트에 semo-core/가 있습니다."));
181
184
  console.log(chalk_1.default.cyan("\n 💡 마이그레이션 방법:"));
182
- console.log(chalk_1.default.gray(" 1. 기존 semo-core/, semo-skills/ 폴더 삭제"));
185
+ console.log(chalk_1.default.gray(" 1. 기존 semo-core/ 폴더 삭제"));
183
186
  console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
184
187
  console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
185
188
  console.log(chalk_1.default.gray(" 또는: semo migrate --force\n"));
@@ -198,19 +201,7 @@ async function showVersionComparison(cwd) {
198
201
  level: 0,
199
202
  });
200
203
  }
201
- // semo-skills (semo-system/ 내부만 확인)
202
- const skillsPathSystem = path.join(semoSystemDir, "semo-skills", "VERSION");
203
- if (fs.existsSync(skillsPathSystem)) {
204
- const localSkills = fs.readFileSync(skillsPathSystem, "utf-8").trim();
205
- const remoteSkills = await getRemoteCoreVersion("semo-skills");
206
- versionInfos.push({
207
- name: "semo-skills",
208
- local: localSkills,
209
- remote: remoteSkills,
210
- needsUpdate: remoteSkills ? isVersionLower(localSkills, remoteSkills) : false,
211
- level: 0,
212
- });
213
- }
204
+ // semo-skills 제거됨 중앙 DB 단일 SoT
214
205
  // semo-agents (semo-system/ 내부)
215
206
  const agentsPathSystem = path.join(semoSystemDir, "semo-agents", "VERSION");
216
207
  if (fs.existsSync(agentsPathSystem)) {
@@ -384,13 +375,13 @@ function isSymlinkValid(linkPath) {
384
375
  }
385
376
  /**
386
377
  * 레거시 SEMO 환경을 감지합니다.
387
- * 레거시: 프로젝트 루트에 semo-core/, semo-skills/ 가 직접 있는 경우
378
+ * 레거시: 프로젝트 루트에 semo-core/ 가 직접 있는 경우
388
379
  * 신규: semo-system/ 하위에 있는 경우
389
380
  */
390
381
  function detectLegacyEnvironment(cwd) {
391
382
  const legacyPaths = [];
392
383
  // 루트에 직접 있는 레거시 디렉토리 확인
393
- const legacyDirs = ["semo-core", "semo-skills", "sax-core", "sax-skills"];
384
+ const legacyDirs = ["semo-core", "sax-core", "sax-skills"];
394
385
  for (const dir of legacyDirs) {
395
386
  const dirPath = path.join(cwd, dir);
396
387
  if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
@@ -452,7 +443,7 @@ async function migrateLegacyEnvironment(cwd) {
452
443
  if (!shouldMigrate) {
453
444
  console.log(chalk_1.default.yellow("\n마이그레이션이 취소되었습니다."));
454
445
  console.log(chalk_1.default.gray("💡 수동 마이그레이션 방법:"));
455
- console.log(chalk_1.default.gray(" 1. 기존 semo-core/, semo-skills/ 폴더 삭제"));
446
+ console.log(chalk_1.default.gray(" 1. 기존 레거시 폴더 삭제"));
456
447
  console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
457
448
  console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
458
449
  return false;
@@ -460,7 +451,7 @@ async function migrateLegacyEnvironment(cwd) {
460
451
  const spinner = (0, ora_1.default)("레거시 환경 마이그레이션 중...").start();
461
452
  try {
462
453
  // 1. 루트의 레거시 디렉토리 삭제
463
- const legacyDirs = ["semo-core", "semo-skills", "sax-core", "sax-skills"];
454
+ const legacyDirs = ["semo-core", "sax-core", "sax-skills"];
464
455
  for (const dir of legacyDirs) {
465
456
  const dirPath = path.join(cwd, dir);
466
457
  if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
@@ -579,21 +570,6 @@ async function showVersionInfo() {
579
570
  level: 0,
580
571
  });
581
572
  }
582
- // 3. semo-skills 버전 (루트 또는 semo-system 내부)
583
- const skillsPathRoot = path.join(cwd, "semo-skills", "VERSION");
584
- const skillsPathSystem = path.join(cwd, "semo-system", "semo-skills", "VERSION");
585
- const skillsPath = fs.existsSync(skillsPathRoot) ? skillsPathRoot : skillsPathSystem;
586
- if (fs.existsSync(skillsPath)) {
587
- const localSkills = fs.readFileSync(skillsPath, "utf-8").trim();
588
- const remoteSkills = await getRemoteCoreVersion("semo-skills");
589
- versionInfos.push({
590
- name: "semo-skills",
591
- local: localSkills,
592
- remote: remoteSkills,
593
- needsUpdate: remoteSkills ? isVersionLower(localSkills, remoteSkills) : false,
594
- level: 0,
595
- });
596
- }
597
573
  // 결과 출력
598
574
  const needsUpdateCount = versionInfos.filter(v => v.needsUpdate).length;
599
575
  if (versionInfos.length === 1) {
@@ -676,11 +652,6 @@ async function confirmOverwrite(itemName, itemPath) {
676
652
  if (!fs.existsSync(itemPath)) {
677
653
  return true;
678
654
  }
679
- // 비인터랙티브 환경(CI, 파이프) — 덮어쓰지 않고 기존 파일 유지
680
- if (!process.stdin.isTTY) {
681
- console.log(chalk_1.default.gray(` → ${itemName} 이미 존재 (비인터랙티브 모드: 건너뜀)`));
682
- return false;
683
- }
684
655
  const { shouldOverwrite } = await inquirer_1.default.prompt([
685
656
  {
686
657
  type: "confirm",
@@ -769,36 +740,82 @@ async function showToolsStatus() {
769
740
  }
770
741
  return true;
771
742
  }
772
- // === init 명령어 ===
743
+ // === 글로벌 설정 체크 ===
744
+ function isGlobalSetupDone() {
745
+ const home = os.homedir();
746
+ return (fs.existsSync(path.join(home, ".semo.env")) &&
747
+ fs.existsSync(path.join(home, ".claude", "skills")));
748
+ }
749
+ // === onboarding 명령어 (글로벌 1회 설정) ===
773
750
  program
774
- .command("init")
775
- .description("현재 프로젝트에 SEMO 설치합니다")
751
+ .command("onboarding")
752
+ .description("글로벌 SEMO 설정 (머신당 1회) — ~/.claude/, ~/.semo.env")
753
+ .option("--credentials-gist <gistId>", "Private GitHub Gist에서 DB 접속정보 가져오기")
776
754
  .option("-f, --force", "기존 설정 덮어쓰기")
777
755
  .option("--skip-mcp", "MCP 설정 생략")
756
+ .action(async (options) => {
757
+ console.log(chalk_1.default.cyan.bold("\n🏠 SEMO 글로벌 온보딩\n"));
758
+ console.log(chalk_1.default.gray(" 대상: ~/.claude/, ~/.semo.env (머신당 1회)\n"));
759
+ // 1. ~/.semo.env DB 접속 설정
760
+ await setupSemoEnv(options.credentialsGist, options.force);
761
+ // 2. DB health check
762
+ const spinner = (0, ora_1.default)("DB 연결 확인 중...").start();
763
+ const connected = await (0, database_1.isDbConnected)();
764
+ if (connected) {
765
+ spinner.succeed("DB 연결 확인됨");
766
+ }
767
+ else {
768
+ spinner.warn("DB 연결 실패 — 스킬/커맨드/에이전트 설치를 건너뜁니다");
769
+ console.log(chalk_1.default.gray(" ~/.semo.env를 확인하고 다시 시도하세요: semo onboarding\n"));
770
+ await (0, database_1.closeConnection)();
771
+ return;
772
+ }
773
+ // 3. Standard 설치 (DB → ~/.claude/skills, commands, agents)
774
+ await setupStandardGlobal();
775
+ // 4. Hooks 설치 (프로젝트 무관)
776
+ await setupHooks(false);
777
+ // 5. MCP 설정 (글로벌 공통 서버)
778
+ if (!options.skipMcp) {
779
+ await setupMCP(os.homedir(), [], options.force || false);
780
+ }
781
+ // 6. semo-kb MCP 유저레벨 등록
782
+ if (!options.skipMcp) {
783
+ await setupSemoKbMcp();
784
+ }
785
+ // 7. 글로벌 CLAUDE.md에 KB-First 규칙 주입
786
+ await injectKbFirstToGlobalClaudeMd();
787
+ await (0, database_1.closeConnection)();
788
+ // 결과 요약
789
+ console.log(chalk_1.default.green.bold("\n✅ SEMO 글로벌 온보딩 완료!\n"));
790
+ console.log(chalk_1.default.cyan("설치된 구성:"));
791
+ console.log(chalk_1.default.gray(" ~/.semo.env DB 접속정보 (권한 600)"));
792
+ console.log(chalk_1.default.gray(" ~/.claude/skills/ 팀 스킬 (DB 기반)"));
793
+ console.log(chalk_1.default.gray(" ~/.claude/commands/ 팀 커맨드 (DB 기반)"));
794
+ console.log(chalk_1.default.gray(" ~/.claude/agents/ 팀 에이전트 (DB 기반, dedup)"));
795
+ console.log(chalk_1.default.gray(" ~/.claude/settings.local.json SessionStart/Stop 훅"));
796
+ console.log(chalk_1.default.gray(" ~/.claude/settings.json semo-kb MCP (유저레벨, KB-First SoT)"));
797
+ console.log(chalk_1.default.cyan("\n다음 단계:"));
798
+ console.log(chalk_1.default.gray(" 프로젝트 디렉토리에서 'semo init'을 실행하세요."));
799
+ console.log();
800
+ });
801
+ // === init 명령어 (프로젝트별 설정) ===
802
+ program
803
+ .command("init")
804
+ .description("현재 프로젝트에 SEMO 프로젝트 설정을 합니다 (글로벌: semo onboarding)")
805
+ .option("-f, --force", "기존 설정 덮어쓰기")
778
806
  .option("--no-gitignore", ".gitignore 수정 생략")
779
- .option("--migrate", "레거시 환경 강제 마이그레이션")
780
- .option("--seed-skills", "semo-system/semo-skills/ → semo.skills DB 초기 시딩")
781
- .option("--credentials-gist <gistId>", "Private GitHub Gist에서 팀 DB 접속정보 자동 가져오기")
782
807
  .action(async (options) => {
783
- console.log(chalk_1.default.cyan.bold("\n🚀 SEMO 설치 시작\n"));
808
+ console.log(chalk_1.default.cyan.bold("\n📁 SEMO 프로젝트 설정\n"));
784
809
  const cwd = process.cwd();
785
- // 0.1. 버전 비교
786
- await showVersionComparison(cwd);
787
- // 0.5. 레거시 환경 감지 및 마이그레이션
788
- const legacyCheck = detectLegacyEnvironment(cwd);
789
- if (legacyCheck.hasLegacy || options.migrate) {
790
- const migrationSuccess = await migrateLegacyEnvironment(cwd);
791
- if (!migrationSuccess) {
792
- process.exit(0);
793
- }
794
- }
795
- // 1. 필수 도구 확인
796
- const shouldContinue = await showToolsStatus();
797
- if (!shouldContinue) {
798
- console.log(chalk_1.default.yellow("\n설치가 취소되었습니다. 필수 도구 설치 후 다시 시도하세요.\n"));
799
- process.exit(0);
810
+ // 0. 글로벌 설정 확인
811
+ if (!isGlobalSetupDone()) {
812
+ console.log(chalk_1.default.yellow("⚠ 글로벌 설정이 완료되지 않았습니다."));
813
+ console.log(chalk_1.default.gray(" 먼저 'semo onboarding'을 실행하세요.\n"));
814
+ console.log(chalk_1.default.gray(" 이 머신에서 처음 SEMO를 사용하시나요?"));
815
+ console.log(chalk_1.default.cyan(" → semo onboarding --credentials-gist <GIST_ID>\n"));
816
+ process.exit(1);
800
817
  }
801
- // 1.5. Git 레포지토리 확인
818
+ // 1. Git 레포지토리 확인
802
819
  const spinner = (0, ora_1.default)("Git 레포지토리 확인 중...").start();
803
820
  try {
804
821
  (0, child_process_1.execSync)("git rev-parse --git-dir", { cwd, stdio: "pipe" });
@@ -814,64 +831,48 @@ program
814
831
  fs.mkdirSync(claudeDir, { recursive: true });
815
832
  console.log(chalk_1.default.green("\n✓ .claude/ 디렉토리 생성됨"));
816
833
  }
817
- // 2.5. ~/.semo.env DB 접속 설정 (자동 감지 → Gist → 프롬프트) — DB 연결 전에 먼저
818
- await setupSemoEnv(options.credentialsGist);
819
- // 3. Standard 설치 (semo-core + semo-skills)
820
- await setupStandard(cwd, options.force);
821
- // 4. MCP 설정
822
- if (!options.skipMcp) {
823
- await setupMCP(cwd, [], options.force);
834
+ // 3. 로컬 스킬 경고 (기존 프로젝트 호환)
835
+ const localSkillsDir = path.join(claudeDir, "skills");
836
+ if (fs.existsSync(localSkillsDir)) {
837
+ try {
838
+ const localSkills = fs.readdirSync(localSkillsDir).filter(f => fs.statSync(path.join(localSkillsDir, f)).isDirectory());
839
+ if (localSkills.length > 0) {
840
+ console.log(chalk_1.default.yellow(`\nℹ 프로젝트 로컬 스킬이 감지되었습니다 (${localSkills.length}개).`));
841
+ console.log(chalk_1.default.gray(" 글로벌 스킬(~/.claude/skills/)이 우선 적용됩니다."));
842
+ console.log(chalk_1.default.gray(" 로컬 스킬을 제거하려면: rm -rf .claude/skills/ .claude/commands/ .claude/agents/\n"));
843
+ }
844
+ }
845
+ catch {
846
+ // ignore
847
+ }
824
848
  }
825
- // 5. Context Mesh 초기화
849
+ // 4. Context Mesh 초기화
826
850
  await setupContextMesh(cwd);
851
+ // 5. CLAUDE.md 생성 (프로젝트 규칙만, 스킬 목록 없음)
852
+ await setupClaudeMd(cwd, [], options.force || false);
827
853
  // 6. .gitignore 업데이트
828
854
  if (options.gitignore !== false) {
829
855
  updateGitignore(cwd);
830
856
  }
831
- // 7. Hooks 설치
832
- await setupHooks(cwd, false);
833
- // 8. CLAUDE.md 생성
834
- await setupClaudeMd(cwd, [], options.force);
835
- // 9. 설치 검증
836
- const verificationResult = verifyInstallation(cwd, []);
837
- printVerificationResult(verificationResult);
838
- // 10. Skills DB 시딩 (--seed-skills 옵션)
839
- if (options.seedSkills) {
840
- console.log(chalk_1.default.cyan("\n🌱 스킬 DB 시딩 (--seed-skills)"));
841
- const semoSystemDir = path.join(cwd, "semo-system");
842
- await seedSkillsToDb(semoSystemDir);
843
- }
844
857
  // 완료 메시지
845
- if (verificationResult.success) {
846
- console.log(chalk_1.default.green.bold("\n✅ SEMO 설치 완료!\n"));
847
- }
848
- else {
849
- console.log(chalk_1.default.yellow.bold("\n⚠️ SEMO 설치 완료 (일부 문제 발견)\n"));
850
- }
851
- console.log(chalk_1.default.cyan("설치된 구성:"));
852
- console.log(chalk_1.default.gray(" ✓ .claude/skills/ (DB 기반 스킬)"));
853
- console.log(chalk_1.default.gray(" ✓ .claude/agents/ (DB 기반 에이전트)"));
854
- console.log(chalk_1.default.gray(" ✓ .claude/commands/ (슬래시 커맨드)"));
855
- console.log(chalk_1.default.gray(" ✓ .claude/memory/ (컨텍스트 동기화)"));
856
- console.log(chalk_1.default.gray(" ✓ ~/.claude/settings.local.json (훅 등록)"));
858
+ console.log(chalk_1.default.green.bold("\n✅ SEMO 프로젝트 설정 완료!\n"));
859
+ console.log(chalk_1.default.cyan("생성된 파일:"));
860
+ console.log(chalk_1.default.gray(" {cwd}/.claude/CLAUDE.md 프로젝트 규칙"));
861
+ console.log(chalk_1.default.gray(" {cwd}/.claude/memory/context.md 프로젝트 상태"));
862
+ console.log(chalk_1.default.gray(" {cwd}/.claude/memory/decisions.md ADR"));
863
+ console.log(chalk_1.default.gray(" {cwd}/.claude/memory/projects.md 프로젝트 맵"));
857
864
  console.log(chalk_1.default.cyan("\n다음 단계:"));
858
865
  console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기 (SessionStart 훅이 자동 sync)"));
859
866
  console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
860
867
  console.log(chalk_1.default.gray(" 3. /SEMO:help로 도움말 확인"));
861
868
  console.log();
862
- console.log(chalk_1.default.gray(" DB 접속정보 변경: nano ~/.semo.env"));
863
- console.log();
864
869
  });
865
- // === Standard 설치 (DB 기반) ===
866
- async function setupStandard(cwd, force) {
867
- const claudeDir = path.join(cwd, ".claude");
868
- console.log(chalk_1.default.cyan("\n📚 Standard 설치 (DB 기반)"));
869
- console.log(chalk_1.default.gray(" 스킬: DB에서 조회하여 파일 생성"));
870
- console.log(chalk_1.default.gray(" 커맨드: DB에서 조회하여 파일 생성"));
871
- console.log(chalk_1.default.gray(" 에이전트: DB에서 조회하여 파일 생성\n"));
870
+ // === Standard 설치 (DB 기반, 글로벌 ~/.claude/) ===
871
+ async function setupStandardGlobal() {
872
+ console.log(chalk_1.default.cyan("\n📚 Standard 설치 (DB → ~/.claude/)"));
873
+ console.log(chalk_1.default.gray(" 스킬/커맨드/에이전트를 글로벌에 설치\n"));
872
874
  const spinner = (0, ora_1.default)("DB에서 스킬/커맨드/에이전트 조회 중...").start();
873
875
  try {
874
- // DB 연결 확인
875
876
  const connected = await (0, database_1.isDbConnected)();
876
877
  if (connected) {
877
878
  spinner.text = "DB 연결 성공, 데이터 조회 중...";
@@ -879,174 +880,18 @@ async function setupStandard(cwd, force) {
879
880
  else {
880
881
  spinner.text = "DB 연결 실패, 폴백 데이터 사용 중...";
881
882
  }
882
- // .claude 디렉토리 생성
883
- fs.mkdirSync(claudeDir, { recursive: true });
884
- // 1. 스킬 설치
885
- const skillsDir = path.join(claudeDir, "skills");
886
- if (force && fs.existsSync(skillsDir)) {
887
- removeRecursive(skillsDir);
888
- }
889
- fs.mkdirSync(skillsDir, { recursive: true });
890
- const skills = await (0, database_1.getActiveSkills)();
891
- for (const skill of skills) {
892
- const skillFolder = path.join(skillsDir, skill.name);
893
- fs.mkdirSync(skillFolder, { recursive: true });
894
- fs.writeFileSync(path.join(skillFolder, "SKILL.md"), skill.content);
895
- }
896
- console.log(chalk_1.default.green(` ✓ skills 설치 완료 (${skills.length}개)`));
897
- // 2. 커맨드 설치
898
- const commandsDir = path.join(claudeDir, "commands");
899
- if (force && fs.existsSync(commandsDir)) {
900
- removeRecursive(commandsDir);
901
- }
902
- fs.mkdirSync(commandsDir, { recursive: true });
903
- const commands = await (0, database_1.getCommands)();
904
- // 폴더별로 그룹핑
905
- const commandsByFolder = {};
906
- for (const cmd of commands) {
907
- if (!commandsByFolder[cmd.folder]) {
908
- commandsByFolder[cmd.folder] = [];
909
- }
910
- commandsByFolder[cmd.folder].push(cmd);
911
- }
912
- let cmdCount = 0;
913
- for (const [folder, cmds] of Object.entries(commandsByFolder)) {
914
- const folderPath = path.join(commandsDir, folder);
915
- fs.mkdirSync(folderPath, { recursive: true });
916
- for (const cmd of cmds) {
917
- fs.writeFileSync(path.join(folderPath, `${cmd.name}.md`), cmd.content);
918
- cmdCount++;
919
- }
920
- }
921
- console.log(chalk_1.default.green(` ✓ commands 설치 완료 (${cmdCount}개)`));
922
- // 3. 에이전트 설치
923
- const agentsDir = path.join(claudeDir, "agents");
924
- if (force && fs.existsSync(agentsDir)) {
925
- removeRecursive(agentsDir);
926
- }
927
- fs.mkdirSync(agentsDir, { recursive: true });
928
- const agents = await (0, database_1.getAgents)();
929
- for (const agent of agents) {
930
- const agentFolder = path.join(agentsDir, agent.name);
931
- fs.mkdirSync(agentFolder, { recursive: true });
932
- fs.writeFileSync(path.join(agentFolder, `${agent.name}.md`), agent.content);
933
- }
934
- console.log(chalk_1.default.green(` ✓ agents 설치 완료 (${agents.length}개)`));
935
- spinner.succeed("Standard 설치 완료 (DB 기반)");
883
+ const result = await (0, global_cache_1.syncGlobalCache)();
884
+ console.log(chalk_1.default.green(` ✓ skills 설치 완료 (${result.skills})`));
885
+ console.log(chalk_1.default.green(` ✓ commands 설치 완료 (${result.commands}개)`));
886
+ console.log(chalk_1.default.green(` ✓ agents 설치 완료 (${result.agents}개)`));
887
+ spinner.succeed("Standard 설치 완료 (DB ~/.claude/)");
936
888
  }
937
889
  catch (error) {
938
890
  spinner.fail("Standard 설치 실패");
939
891
  console.error(chalk_1.default.red(` ${error}`));
940
892
  }
941
893
  }
942
- // === CLAUDE.md 생성 (DB 기반) ===
943
- async function generateClaudeMd(cwd) {
944
- console.log(chalk_1.default.cyan("\n📄 CLAUDE.md 생성"));
945
- const claudeMdPath = path.join(cwd, ".claude", "CLAUDE.md");
946
- const skills = await (0, database_1.getActiveSkills)();
947
- const skillCategories = await (0, database_1.getSkillCountByCategory)();
948
- const skillList = Object.entries(skillCategories)
949
- .map(([cat, count]) => ` - ${cat}: ${count}개`)
950
- .join("\n");
951
- const claudeMdContent = `# SEMO Project Configuration
952
-
953
- > SEMO (Semicolon Orchestrate) - AI Agent Orchestration Framework v3.14.0
954
-
955
- ---
956
-
957
- ## 🔴 MANDATORY: Memory Context (항시 참조)
958
-
959
- > **⚠️ 세션 시작 시 반드시 \`.claude/memory/\` 폴더의 파일들을 먼저 읽으세요. 예외 없음.**
960
-
961
- ### 필수 참조 파일
962
-
963
- \`\`\`
964
- .claude/memory/
965
- ├── context.md # 프로젝트 상태, 기술 스택, 진행 중 작업
966
- ├── decisions.md # 아키텍처 결정 기록 (ADR)
967
- ├── projects.md # GitHub Projects 설정
968
- └── rules/ # 프로젝트별 커스텀 규칙
969
- \`\`\`
970
-
971
- **이 파일들은 세션의 컨텍스트를 유지하는 장기 기억입니다. 매 세션마다 반드시 읽고 시작하세요.**
972
-
973
- ---
974
-
975
- ## 🔴 MANDATORY: Orchestrator-First Execution
976
-
977
- > **⚠️ 이 규칙은 모든 사용자 요청에 적용됩니다. 예외 없음.**
978
-
979
- ### 실행 흐름 (필수)
980
-
981
- \`\`\`
982
- 1. 사용자 요청 수신
983
- 2. Orchestrator가 의도 분석 후 적절한 Agent/Skill 라우팅
984
- 3. Agent/Skill이 작업 수행
985
- 4. 실행 결과 반환
986
- \`\`\`
987
-
988
- ### Orchestrator 참조
989
-
990
- **Primary Orchestrator**: \`.claude/agents/orchestrator/orchestrator.md\`
991
-
992
- 이 파일에서 라우팅 테이블, 의도 분류, 메시지 포맷을 확인하세요.
993
-
994
- ---
995
-
996
- ## 🔴 NON-NEGOTIABLE RULES
997
-
998
- ### 1. Orchestrator-First Policy
999
-
1000
- > **모든 요청은 반드시 Orchestrator를 통해 라우팅됩니다. 직접 처리 금지.**
1001
-
1002
- **직접 처리 금지 항목**:
1003
- - 코드 작성/수정 → \`write-code\` 스킬
1004
- - Git 커밋/푸시 → \`git-workflow\` 스킬
1005
- - 품질 검증 → \`quality-gate\` 스킬
1006
-
1007
- ### 2. Pre-Commit Quality Gate
1008
-
1009
- > **코드 변경이 포함된 커밋 전 반드시 Quality Gate를 통과해야 합니다.**
1010
-
1011
- \`\`\`bash
1012
- # 필수 검증 순서
1013
- npm run lint # 1. ESLint 검사
1014
- npx tsc --noEmit # 2. TypeScript 타입 체크
1015
- npm run build # 3. 빌드 검증
1016
- \`\`\`
1017
-
1018
- ---
1019
-
1020
- ## 설치된 구성
1021
-
1022
- ### 스킬 (${skills.length}개)
1023
- ${skillList}
1024
-
1025
- ## 구조
1026
-
1027
- \`\`\`
1028
- .claude/
1029
- ├── settings.json # MCP 서버 설정
1030
- ├── agents/ # 에이전트 (DB 기반 설치)
1031
- ├── skills/ # 스킬 (DB 기반 설치)
1032
- └── commands/ # 커맨드 (DB 기반 설치)
1033
- \`\`\`
1034
-
1035
- ## 사용 가능한 커맨드
1036
-
1037
- | 커맨드 | 설명 |
1038
- |--------|------|
1039
- | \`/SEMO:help\` | 도움말 |
1040
- | \`/SEMO:dry-run {프롬프트}\` | 명령 검증 (라우팅 시뮬레이션) |
1041
- | \`/SEMO-workflow:greenfield\` | Greenfield 워크플로우 시작 |
1042
-
1043
- ---
1044
-
1045
- > Generated by SEMO CLI v3.14.0 (DB-based installation)
1046
- `;
1047
- fs.writeFileSync(claudeMdPath, claudeMdContent);
1048
- console.log(chalk_1.default.green("✓ .claude/CLAUDE.md 생성됨"));
1049
- }
894
+ // (generateClaudeMd removed — setupClaudeMd handles project CLAUDE.md generation)
1050
895
  // === Standard 심볼릭 링크 (레거시 호환) ===
1051
896
  async function createStandardSymlinks(cwd) {
1052
897
  const claudeDir = path.join(cwd, ".claude");
@@ -1070,29 +915,9 @@ async function createStandardSymlinks(cwd) {
1070
915
  }
1071
916
  console.log(chalk_1.default.green(` ✓ .claude/agents/ (${agents.length}개 agent 링크됨)`));
1072
917
  }
1073
- // skills 디렉토리 생성 개별 링크 (DB 기반 - 활성 스킬만)
918
+ // skills 중앙 DB 단일 SoT (semo-skills 파일시스템 제거됨)
1074
919
  const claudeSkillsDir = path.join(claudeDir, "skills");
1075
- const coreSkillsDir = path.join(semoSystemDir, "semo-skills");
1076
- if (fs.existsSync(coreSkillsDir)) {
1077
- // 기존 심볼릭 링크면 삭제 (디렉토리로 변경)
1078
- if (fs.existsSync(claudeSkillsDir) && fs.lstatSync(claudeSkillsDir).isSymbolicLink()) {
1079
- removeRecursive(claudeSkillsDir);
1080
- }
1081
- fs.mkdirSync(claudeSkillsDir, { recursive: true });
1082
- // DB에서 활성 스킬 목록 조회 (19개 핵심 스킬만)
1083
- const activeSkillNames = await (0, database_1.getActiveSkillNames)();
1084
- let linkedCount = 0;
1085
- for (const skillName of activeSkillNames) {
1086
- const skillLink = path.join(claudeSkillsDir, skillName);
1087
- const skillTarget = path.join(coreSkillsDir, skillName);
1088
- // 스킬 폴더가 존재하는 경우에만 링크
1089
- if (fs.existsSync(skillTarget) && !fs.existsSync(skillLink)) {
1090
- createSymlinkOrJunction(skillTarget, skillLink);
1091
- linkedCount++;
1092
- }
1093
- }
1094
- console.log(chalk_1.default.green(` ✓ .claude/skills/ (${linkedCount}개 skill 링크됨 - DB 기반)`));
1095
- }
920
+ fs.mkdirSync(claudeSkillsDir, { recursive: true });
1096
921
  // commands 링크
1097
922
  const commandsDir = path.join(claudeDir, "commands");
1098
923
  fs.mkdirSync(commandsDir, { recursive: true });
@@ -1183,15 +1008,10 @@ function verifyInstallation(cwd, installedExtensions = []) {
1183
1008
  }
1184
1009
  // === 레거시: semo-system 기반 설치 검증 ===
1185
1010
  const coreDir = path.join(semoSystemDir, "semo-core");
1186
- const skillsDir = path.join(semoSystemDir, "semo-skills");
1187
1011
  if (!fs.existsSync(coreDir)) {
1188
1012
  result.errors.push("semo-core가 설치되지 않았습니다");
1189
1013
  result.success = false;
1190
1014
  }
1191
- if (!fs.existsSync(skillsDir)) {
1192
- result.errors.push("semo-skills가 설치되지 않았습니다");
1193
- result.success = false;
1194
- }
1195
1015
  // 2. agents 링크 검증 (isSymlinkValid 사용)
1196
1016
  const claudeAgentsDir = path.join(claudeDir, "agents");
1197
1017
  const coreAgentsDir = path.join(coreDir, "agents");
@@ -1218,31 +1038,7 @@ function verifyInstallation(cwd, installedExtensions = []) {
1218
1038
  }
1219
1039
  }
1220
1040
  }
1221
- // 3. skills 링크 검증 (isSymlinkValid 사용)
1222
- if (fs.existsSync(skillsDir)) {
1223
- const expectedSkills = fs.readdirSync(skillsDir).filter(f => fs.statSync(path.join(skillsDir, f)).isDirectory());
1224
- result.stats.skills.expected = expectedSkills.length;
1225
- const claudeSkillsDir = path.join(claudeDir, "skills");
1226
- if (fs.existsSync(claudeSkillsDir)) {
1227
- for (const skill of expectedSkills) {
1228
- const linkPath = path.join(claudeSkillsDir, skill);
1229
- try {
1230
- if (fs.existsSync(linkPath) || fs.lstatSync(linkPath).isSymbolicLink()) {
1231
- if (isSymlinkValid(linkPath)) {
1232
- result.stats.skills.linked++;
1233
- }
1234
- else {
1235
- result.stats.skills.broken++;
1236
- result.warnings.push(`깨진 링크: .claude/skills/${skill}`);
1237
- }
1238
- }
1239
- }
1240
- catch {
1241
- // 링크가 존재하지 않음
1242
- }
1243
- }
1244
- }
1245
- }
1041
+ // 3. skills 중앙 DB 단일 SoT (파일시스템 검증 불필요)
1246
1042
  // 4. commands 검증 (isSymlinkValid 사용)
1247
1043
  const semoCommandsLink = path.join(claudeDir, "commands", "SEMO");
1248
1044
  try {
@@ -1372,110 +1168,156 @@ const BASE_MCP_SERVERS = [
1372
1168
  name: "context7",
1373
1169
  command: "npx",
1374
1170
  args: ["-y", "@upstash/context7-mcp"],
1171
+ scope: "user",
1375
1172
  },
1376
1173
  {
1377
1174
  name: "sequential-thinking",
1378
1175
  command: "npx",
1379
1176
  args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
1177
+ scope: "user",
1380
1178
  },
1381
1179
  {
1382
1180
  name: "playwright",
1383
1181
  command: "npx",
1384
1182
  args: ["-y", "@anthropic-ai/mcp-server-playwright"],
1183
+ scope: "user",
1385
1184
  },
1386
1185
  {
1387
1186
  name: "github",
1388
1187
  command: "npx",
1389
1188
  args: ["-y", "@modelcontextprotocol/server-github"],
1189
+ scope: "user",
1190
+ },
1191
+ ];
1192
+ const SEMO_CREDENTIALS = [
1193
+ {
1194
+ key: "DATABASE_URL",
1195
+ required: true,
1196
+ sensitive: true,
1197
+ description: "팀 코어 PostgreSQL 연결 URL",
1198
+ promptMessage: "DATABASE_URL:",
1199
+ },
1200
+ {
1201
+ key: "OPENAI_API_KEY",
1202
+ required: false,
1203
+ sensitive: true,
1204
+ description: "OpenAI API 키 (KB 임베딩용)",
1205
+ promptMessage: "OPENAI_API_KEY (없으면 Enter):",
1206
+ },
1207
+ {
1208
+ key: "SLACK_WEBHOOK",
1209
+ required: false,
1210
+ sensitive: false,
1211
+ description: "Slack 알림 Webhook (선택)",
1212
+ promptMessage: "SLACK_WEBHOOK (없으면 Enter):",
1390
1213
  },
1391
1214
  ];
1392
- // === ~/.semo.env 설정 (자동 감지 → Gist → 프롬프트) ===
1393
- function writeSemoEnvFile(dbUrl, slackWebhook = "") {
1215
+ function writeSemoEnvFile(creds) {
1394
1216
  const envFile = path.join(os.homedir(), ".semo.env");
1395
- const content = `# SEMO 환경변수 — 모든 컨텍스트에서 자동 로드됨
1396
- # (Claude Code 앱, OpenClaw LaunchAgent, cron 등 비인터랙티브 환경 포함)
1397
-
1398
- DATABASE_URL='${dbUrl}'
1399
-
1400
- # Slack 알림 Webhook (선택 — bot-ops 채널 dead-letter 감지용)
1401
- SLACK_WEBHOOK='${slackWebhook}'
1402
- `;
1403
- fs.writeFileSync(envFile, content, { mode: 0o600 });
1217
+ const lines = [
1218
+ "# SEMO 환경변수 모든 컨텍스트에서 자동 로드됨",
1219
+ "# (Claude Code 앱, OpenClaw LaunchAgent, cron 등)",
1220
+ "",
1221
+ ];
1222
+ // 레지스트리 먼저 (순서 보장)
1223
+ for (const def of SEMO_CREDENTIALS) {
1224
+ const val = creds[def.key] || "";
1225
+ lines.push(`# ${def.description}`);
1226
+ lines.push(`${def.key}='${val}'`);
1227
+ lines.push("");
1228
+ }
1229
+ // 레지스트리 외 추가 키
1230
+ for (const [k, v] of Object.entries(creds)) {
1231
+ if (!SEMO_CREDENTIALS.some((d) => d.key === k)) {
1232
+ lines.push(`${k}='${v}'`);
1233
+ }
1234
+ }
1235
+ lines.push("");
1236
+ fs.writeFileSync(envFile, lines.join("\n"), { mode: 0o600 });
1404
1237
  }
1405
- function readSemoEnvDbUrl() {
1238
+ function readSemoEnvCreds() {
1406
1239
  const envFile = path.join(os.homedir(), ".semo.env");
1407
1240
  if (!fs.existsSync(envFile))
1408
- return null;
1409
- const content = fs.readFileSync(envFile, "utf-8");
1410
- const match = content.match(/^DATABASE_URL='([^']+)'/m) || content.match(/^DATABASE_URL="([^"]+)"/m);
1411
- return match ? match[1] : null;
1241
+ return {};
1242
+ try {
1243
+ return (0, env_parser_1.parseEnvContent)(fs.readFileSync(envFile, "utf-8"));
1244
+ }
1245
+ catch {
1246
+ return {};
1247
+ }
1412
1248
  }
1413
- function fetchDbUrlFromGist(gistId) {
1249
+ function fetchCredsFromGist(gistId) {
1414
1250
  try {
1415
1251
  const raw = (0, child_process_1.execSync)(`gh gist view ${gistId} --raw`, {
1416
1252
  encoding: "utf-8",
1417
1253
  stdio: ["pipe", "pipe", "pipe"],
1418
1254
  timeout: 8000,
1419
1255
  });
1420
- // Gist 파일 형식: DATABASE_URL=<url> 또는 단순 URL
1421
- const match = raw.match(/DATABASE_URL=['"]?([^'"\s]+)['"]?/) || raw.match(/^(postgres(?:ql)?:\/\/[^\s]+)/m);
1422
- return match ? match[1].trim() : null;
1256
+ const creds = (0, env_parser_1.parseEnvContent)(raw);
1257
+ return Object.keys(creds).length > 0 ? creds : null;
1423
1258
  }
1424
1259
  catch {
1425
1260
  return null;
1426
1261
  }
1427
1262
  }
1428
- async function setupSemoEnv(credentialsGist) {
1429
- const envFile = path.join(os.homedir(), ".semo.env");
1430
- console.log(chalk_1.default.cyan("\n🔑 DB 접속 설정"));
1431
- // 1. 이미 env var로 설정됨
1432
- if (process.env.DATABASE_URL) {
1433
- console.log(chalk_1.default.green(" ✅ DATABASE_URL (환경변수)"));
1434
- if (!readSemoEnvDbUrl()) {
1435
- writeSemoEnvFile(process.env.DATABASE_URL);
1436
- console.log(chalk_1.default.gray(` → ~/.semo.env 에 저장됨 (비인터랙티브 환경용)`));
1437
- }
1438
- return;
1439
- }
1440
- // 2. ~/.semo.env 에 이미 있음
1441
- const existing = readSemoEnvDbUrl();
1442
- if (existing) {
1443
- console.log(chalk_1.default.green(" ✅ DATABASE_URL (~/.semo.env)"));
1444
- return;
1445
- }
1446
- // 3. Private GitHub Gist 자동 fetch
1263
+ async function setupSemoEnv(credentialsGist, force) {
1264
+ console.log(chalk_1.default.cyan("\n🔑 환경변수 설정"));
1265
+ // 1. 기존 파일
1266
+ const existing = force ? {} : readSemoEnvCreds();
1267
+ // 2. Gist
1447
1268
  const gistId = credentialsGist || process.env.SEMO_CREDENTIALS_GIST;
1269
+ let gistCreds = {};
1448
1270
  if (gistId) {
1449
1271
  console.log(chalk_1.default.gray(" GitHub Gist에서 팀 접속정보 가져오는 중..."));
1450
- const url = fetchDbUrlFromGist(gistId);
1451
- if (url) {
1452
- writeSemoEnvFile(url);
1453
- console.log(chalk_1.default.green(` ✅ Gist (${gistId.slice(0, 8)}...)에서 DATABASE_URL 설정됨`));
1454
- return;
1272
+ gistCreds = fetchCredsFromGist(gistId) || {};
1273
+ }
1274
+ // 3. 머지: gist(base) ← file(override) ← env(override)
1275
+ const merged = { ...gistCreds };
1276
+ for (const [k, v] of Object.entries(existing)) {
1277
+ if (v)
1278
+ merged[k] = v;
1279
+ }
1280
+ for (const def of SEMO_CREDENTIALS) {
1281
+ if (process.env[def.key])
1282
+ merged[def.key] = process.env[def.key];
1283
+ }
1284
+ // 4. required인데 없는 키만 프롬프트
1285
+ let hasNewKeys = false;
1286
+ for (const def of SEMO_CREDENTIALS) {
1287
+ if (merged[def.key]) {
1288
+ const label = def.sensitive ? "(설정됨)" : merged[def.key];
1289
+ console.log(chalk_1.default.green(` ✅ ${def.key} ${label}`));
1290
+ }
1291
+ else if (def.required) {
1292
+ const { value } = await inquirer_1.default.prompt([
1293
+ {
1294
+ type: "password",
1295
+ name: "value",
1296
+ message: def.promptMessage,
1297
+ mask: "*",
1298
+ },
1299
+ ]);
1300
+ if (value?.trim()) {
1301
+ merged[def.key] = value.trim();
1302
+ hasNewKeys = true;
1303
+ }
1304
+ }
1305
+ else {
1306
+ console.log(chalk_1.default.gray(` ⏭ ${def.key} (없음 — 선택사항)`));
1455
1307
  }
1456
- console.log(chalk_1.default.yellow(" ⚠️ Gist fetch 실패 — 수동 입력으로 전환"));
1457
1308
  }
1458
- // 4. 인터랙티브 프롬프트
1459
- console.log(chalk_1.default.gray(" 팀 Core DB URL을 붙여넣으세요 (나중에 ~/.semo.env에서 수정 가능)"));
1460
- const { dbUrl } = await inquirer_1.default.prompt([
1461
- {
1462
- type: "password",
1463
- name: "dbUrl",
1464
- message: "DATABASE_URL:",
1465
- mask: "*",
1466
- },
1467
- ]);
1468
- if (dbUrl && dbUrl.trim()) {
1469
- writeSemoEnvFile(dbUrl.trim());
1309
+ // 5. 변경사항이 있거나 파일이 없으면 쓰기
1310
+ const envFile = path.join(os.homedir(), ".semo.env");
1311
+ const needsWrite = force ||
1312
+ hasNewKeys ||
1313
+ !fs.existsSync(envFile) ||
1314
+ Object.keys(gistCreds).some((k) => !existing[k]);
1315
+ if (needsWrite) {
1316
+ writeSemoEnvFile(merged);
1470
1317
  console.log(chalk_1.default.green(" ✅ ~/.semo.env 저장됨 (권한: 600)"));
1471
- console.log(chalk_1.default.gray(` 파일: ${envFile}`));
1472
1318
  }
1473
1319
  else {
1474
- // 템플릿 생성
1475
- if (!fs.existsSync(envFile)) {
1476
- writeSemoEnvFile("");
1477
- }
1478
- console.log(chalk_1.default.yellow(" ⚠️ 건너뜀 — ~/.semo.env에서 DATABASE_URL을 직접 입력하세요"));
1320
+ console.log(chalk_1.default.gray(" ~/.semo.env 변경 없음"));
1479
1321
  }
1480
1322
  }
1481
1323
  // === Claude MCP 서버 존재 여부 확인 ===
@@ -1504,6 +1346,9 @@ function registerMCPServer(server) {
1504
1346
  args.push("-e", `${key}=${value}`);
1505
1347
  }
1506
1348
  }
1349
+ // scope 지정 (기본: project)
1350
+ const scope = server.scope || "project";
1351
+ args.push("-s", scope);
1507
1352
  // -- 구분자 후 명령어와 인자 추가
1508
1353
  args.push("--", server.command, ...server.args);
1509
1354
  (0, child_process_1.execSync)(`claude ${args.join(" ")}`, { stdio: "pipe" });
@@ -1513,6 +1358,96 @@ function registerMCPServer(server) {
1513
1358
  return { success: false, error: String(error) };
1514
1359
  }
1515
1360
  }
1361
+ // === 글로벌 CLAUDE.md에 KB-First 규칙 주입 ===
1362
+ const KB_FIRST_SECTION_MARKER = "## SEMO KB-First 행동 규칙";
1363
+ async function injectKbFirstToGlobalClaudeMd() {
1364
+ const globalClaudeMd = path.join(os.homedir(), ".claude", "CLAUDE.md");
1365
+ const kbFirstBlock = `
1366
+ ${KB_FIRST_SECTION_MARKER}
1367
+
1368
+ > semo-kb MCP 서버가 유저레벨에 등록되어 있습니다. KB는 팀의 Single Source of Truth입니다.
1369
+
1370
+ ### 읽기 (Query-First)
1371
+ 다음 주제 질문 → **반드시 kb_search/kb_get으로 KB 먼저 조회** 후 답변:
1372
+ - 팀원 정보 → \`domain: team\`
1373
+ - 프로젝트 현황 → \`domain: project\`
1374
+ - 의사결정 기록 → \`domain: decision\`
1375
+ - 업무 프로세스 → \`domain: process\`
1376
+ - 인프라 구성 → \`domain: infra\`
1377
+ - KPI → \`domain: kpi\`
1378
+
1379
+ **금지:** 위 주제를 자체 지식/세션 기억만으로 답변하는 것.
1380
+ KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
1381
+
1382
+ ### 쓰기 (Write-Back)
1383
+ 사용자가 팀 정보를 정정/추가/변경하면, 의사결정이 내려지면 → **반드시 kb_upsert로 KB에 즉시 기록.**
1384
+ **금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
1385
+ `;
1386
+ if (fs.existsSync(globalClaudeMd)) {
1387
+ const content = fs.readFileSync(globalClaudeMd, "utf-8");
1388
+ if (content.includes(KB_FIRST_SECTION_MARKER)) {
1389
+ // 기존 섹션 교체
1390
+ const regex = new RegExp(`\\n${KB_FIRST_SECTION_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?(?=\\n## |$)`, "m");
1391
+ const updated = content.replace(regex, kbFirstBlock);
1392
+ fs.writeFileSync(globalClaudeMd, updated);
1393
+ console.log(chalk_1.default.gray(" ~/.claude/CLAUDE.md KB-First 규칙 업데이트됨"));
1394
+ }
1395
+ else {
1396
+ // 끝에 추가
1397
+ fs.writeFileSync(globalClaudeMd, content.trimEnd() + "\n" + kbFirstBlock);
1398
+ console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md에 KB-First 규칙 추가됨"));
1399
+ }
1400
+ }
1401
+ else {
1402
+ // 파일 없으면 생성
1403
+ fs.writeFileSync(globalClaudeMd, kbFirstBlock.trim() + "\n");
1404
+ console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md 생성됨 (KB-First 규칙)"));
1405
+ }
1406
+ }
1407
+ // === semo-kb MCP 유저레벨 등록 ===
1408
+ async function setupSemoKbMcp() {
1409
+ console.log(chalk_1.default.cyan("\n📡 semo-kb MCP 유저레벨 등록"));
1410
+ console.log(chalk_1.default.gray(" KB-First SoT — 어디서든 KB 조회/갱신 가능\n"));
1411
+ // semo-kb MCP 서버 경로 탐색
1412
+ // 1. cwd에서 packages/mcp-kb/dist/index.js 찾기
1413
+ // 2. CLI 패키지 기준으로 monorepo 루트 탐색
1414
+ // 3. 환경변수 SEMO_PROJECT_ROOT
1415
+ const candidates = [
1416
+ path.join(process.cwd(), "packages", "mcp-kb", "dist", "index.js"),
1417
+ process.env.SEMO_PROJECT_ROOT
1418
+ ? path.join(process.env.SEMO_PROJECT_ROOT, "packages", "mcp-kb", "dist", "index.js")
1419
+ : "",
1420
+ path.resolve(__dirname, "..", "..", "..", "mcp-kb", "dist", "index.js"),
1421
+ ].filter(Boolean);
1422
+ const mcpEntryPath = candidates.find((p) => fs.existsSync(p));
1423
+ if (!mcpEntryPath) {
1424
+ console.log(chalk_1.default.yellow(" ⚠ semo-kb MCP 서버를 찾을 수 없습니다."));
1425
+ console.log(chalk_1.default.gray(" semo 프로젝트 루트에서 실행하거나 SEMO_PROJECT_ROOT 환경변수를 설정하세요."));
1426
+ console.log(chalk_1.default.gray(" 예: cd /path/to/semo && semo onboarding"));
1427
+ return;
1428
+ }
1429
+ const absolutePath = path.resolve(mcpEntryPath);
1430
+ console.log(chalk_1.default.gray(` 경로: ${absolutePath}`));
1431
+ // claude mcp add로 유저레벨 등록
1432
+ const result = registerMCPServer({
1433
+ name: "semo-kb",
1434
+ command: "node",
1435
+ args: [absolutePath],
1436
+ scope: "user",
1437
+ });
1438
+ if (result.success) {
1439
+ if (result.skipped) {
1440
+ console.log(chalk_1.default.gray(" semo-kb 이미 등록됨 (건너뜀)"));
1441
+ }
1442
+ else {
1443
+ console.log(chalk_1.default.green(" ✓ semo-kb MCP 유저레벨 등록 완료"));
1444
+ }
1445
+ }
1446
+ else {
1447
+ console.log(chalk_1.default.yellow(` ⚠ semo-kb 등록 실패: ${result.error}`));
1448
+ console.log(chalk_1.default.gray(" 수동 등록: claude mcp add semo-kb -s user -- node " + absolutePath));
1449
+ }
1450
+ }
1516
1451
  // === MCP 설정 ===
1517
1452
  async function setupMCP(cwd, _extensions, force) {
1518
1453
  console.log(chalk_1.default.cyan("\n🔧 Black Box 설정 (MCP Server)"));
@@ -1529,23 +1464,13 @@ async function setupMCP(cwd, _extensions, force) {
1529
1464
  const settings = {
1530
1465
  mcpServers: {},
1531
1466
  };
1532
- // MCP 서버 목록 수집
1533
- const allServers = [...BASE_MCP_SERVERS];
1534
- // settings.json에 mcpServers 저장 (백업용)
1535
- for (const server of allServers) {
1536
- const serverConfig = {
1537
- command: server.command,
1538
- args: server.args,
1539
- };
1540
- if (server.env) {
1541
- serverConfig.env = server.env;
1542
- }
1543
- settings.mcpServers[server.name] = serverConfig;
1544
- }
1467
+ // semo-kb는 유저레벨에서 등록 (semo onboarding)하므로 프로젝트 settings에 쓰지 않음
1468
+ // 공통 서버(context7 등)도 유저레벨에 등록하므로 프로젝트 settings에 쓰지 않음
1545
1469
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1546
- console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨 (MCP 설정 백업)"));
1547
- // Claude Code에 MCP 서버 등록 시도
1470
+ console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨"));
1471
+ // Claude Code에 MCP 서버 등록 시도 (공통 서버는 유저레벨로)
1548
1472
  console.log(chalk_1.default.cyan("\n🔌 Claude Code에 MCP 서버 등록 중..."));
1473
+ const allServers = [...BASE_MCP_SERVERS];
1549
1474
  const successServers = [];
1550
1475
  const skippedServers = [];
1551
1476
  const failedServers = [];
@@ -1628,12 +1553,13 @@ semo-system/
1628
1553
  }
1629
1554
  }
1630
1555
  // === Hooks 설치/업데이트 ===
1631
- async function setupHooks(cwd, isUpdate = false) {
1556
+ async function setupHooks(isUpdate = false) {
1632
1557
  const action = isUpdate ? "업데이트" : "설치";
1633
1558
  console.log(chalk_1.default.cyan(`\n🪝 Claude Code Hooks ${action}`));
1634
- const homeDir = process.env.HOME || process.env.USERPROFILE || "";
1559
+ console.log(chalk_1.default.gray(" semo CLI 기반 컨텍스트 동기화\n"));
1560
+ const homeDir = os.homedir();
1635
1561
  const settingsPath = path.join(homeDir, ".claude", "settings.local.json");
1636
- // Core 훅: semo context sync/push — semo-hooks 유무와 무관하게 항상 등록
1562
+ // hooks 설정 객체 — semo CLI만 사용 (프로젝트 경로 무관)
1637
1563
  const hooksConfig = {
1638
1564
  SessionStart: [
1639
1565
  {
@@ -1660,36 +1586,6 @@ async function setupHooks(cwd, isUpdate = false) {
1660
1586
  },
1661
1587
  ],
1662
1588
  };
1663
- // semo-hooks 빌드 (선택적 — semo-system 레포에서만 동작)
1664
- const hooksDir = path.join(cwd, "semo-system", "semo-hooks");
1665
- if (fs.existsSync(hooksDir)) {
1666
- let hooksBuilt = false;
1667
- try {
1668
- (0, child_process_1.execSync)("npm install", { cwd: hooksDir, stdio: ["pipe", "pipe", "pipe"] });
1669
- (0, child_process_1.execSync)("npm run build", { cwd: hooksDir, stdio: ["pipe", "pipe", "pipe"] });
1670
- hooksBuilt = true;
1671
- }
1672
- catch {
1673
- console.log(chalk_1.default.yellow(" ⚠ semo-hooks 빌드 실패 (core 훅만 등록)"));
1674
- }
1675
- if (hooksBuilt) {
1676
- const hooksCmd = `node ${path.join(hooksDir, "dist", "index.js")}`;
1677
- hooksConfig.SessionStart.unshift({
1678
- matcher: "",
1679
- hooks: [{ type: "command", command: `${hooksCmd} session-start`, timeout: 10 }],
1680
- });
1681
- hooksConfig.UserPromptSubmit = [
1682
- { matcher: "", hooks: [{ type: "command", command: `${hooksCmd} user-prompt`, timeout: 5 }] },
1683
- ];
1684
- hooksConfig.Stop.unshift({
1685
- matcher: "",
1686
- hooks: [{ type: "command", command: `${hooksCmd} stop`, timeout: 10 }],
1687
- });
1688
- hooksConfig.SessionEnd = [
1689
- { matcher: "", hooks: [{ type: "command", command: `${hooksCmd} session-end`, timeout: 10 }] },
1690
- ];
1691
- }
1692
- }
1693
1589
  // 기존 설정 로드 또는 새로 생성
1694
1590
  let existingSettings = {};
1695
1591
  const claudeConfigDir = path.join(homeDir, ".claude");
@@ -1890,132 +1786,86 @@ async function setupClaudeMd(cwd, _extensions, force) {
1890
1786
  return;
1891
1787
  }
1892
1788
  }
1893
- const projectName = path.basename(cwd);
1894
- const installDate = new Date().toISOString().split("T")[0];
1895
- const claudeMdContent = `# ${projectName} — Claude Configuration
1896
-
1897
- > SEMO v${VERSION} 설치됨 (${installDate})
1898
-
1899
- ---
1900
-
1901
- ## SEMO란?
1902
-
1903
- **SEMO (Semicolon Orchestrate)** 는 OpenClaw 봇팀과 로컬 Claude Code 세션이
1904
- **팀 Core DB를 단일 진실 공급원(Single Source of Truth)으로 공유**하는 컨텍스트 동기화 시스템이다.
1905
-
1906
- \`\`\`
1907
- 로컬 Claude Code 세션
1908
- ↕ semo context sync / push
1909
- 팀 Core DB (PostgreSQL, semo 스키마)
1910
- ↕ 봇 세션 시작/종료 훅
1911
- OpenClaw 봇팀 (7개 봇)
1912
- workclaw · reviewclaw · planclaw · designclaw
1913
- infraclaw · growthclaw · semiclaw
1914
- \`\`\`
1915
-
1916
- **이 CLAUDE.md가 설치된 프로젝트는 OpenClaw 봇팀의 컨텍스트를 실시간으로 공유받는다.**
1917
-
1918
- ---
1919
-
1920
- ## 자동 동기화
1921
-
1922
- 세션 시작/종료 시 팀 Core DB와 자동 동기화됩니다.
1789
+ // 프로젝트 규칙만 (스킬/에이전트 목록은 글로벌 ~/.claude/에 있음)
1790
+ const claudeMdContent = `# SEMO Project Configuration
1923
1791
 
1924
- | 시점 | 동작 |
1925
- |------|------|
1926
- | 세션 시작 | \`semo context sync\` → \`.claude/memory/\` 최신화 |
1927
- | 세션 종료 | \`semo context push\` → \`decisions.md\` 변경분 DB 저장 |
1792
+ > SEMO (Semicolon Orchestrate) - AI Agent Orchestration Framework v${VERSION}
1793
+ > 스킬/에이전트/커맨드는 글로벌(~/.claude/)에서 로드됩니다.
1928
1794
 
1929
1795
  ---
1930
1796
 
1931
- ## Memory Context
1932
-
1933
- \`.claude/memory/\` 파일들은 **팀 Core DB (\`semo\` 스키마)에서 자동으로 채워집니다**.
1934
- 직접 편집하지 마세요 — 세션 시작 시 덮어씌워집니다.
1797
+ ## KB 접근 (semo-kb MCP 서버)
1935
1798
 
1936
- | 파일 | DB 소스 | 방향 |
1937
- |------|---------|------|
1938
- | \`team.md\` | \`kb WHERE domain='team'\` | DB → 로컬 (읽기 전용) |
1939
- | \`projects.md\` | \`kb WHERE domain='project'\` | DB → 로컬 (읽기 전용) |
1940
- | \`decisions.md\` | \`kb WHERE domain='decision'\` | **양방향** (편집 가능, Stop 시 DB 저장) |
1941
- | \`infra.md\` | \`kb WHERE domain='infra'\` | DB → 로컬 (읽기 전용) |
1942
- | \`process.md\` | \`kb WHERE domain='process'\` | DB → 로컬 (읽기 전용) |
1943
- | \`bots.md\` | \`semo.bot_status\` | DB → 로컬 (봇 상태) |
1944
- | \`ontology.md\` | \`semo.ontology\` | DB → 로컬 (읽기 전용) |
1799
+ KB 데이터는 **semo-kb MCP 서버**를 통해 Core DB에서 실시간 조회합니다.
1945
1800
 
1946
- **decisions.md 편집 가능합니다.** 아키텍처 결정(ADR)을 여기에 기록하세요.
1801
+ | MCP 도구 | 설명 |
1802
+ |----------|------|
1803
+ | \`kb_search\` | 벡터+텍스트 하이브리드 검색 (query, domain?, limit?, mode?) |
1804
+ | \`kb_get\` | domain+key 정확 조회 |
1805
+ | \`kb_list\` | 도메인별 엔트리 목록 |
1806
+ | \`kb_upsert\` | KB 항목 쓰기 (OpenAI 임베딩 자동 생성) |
1807
+ | \`kb_bot_status\` | 봇 상태 테이블 조회 |
1808
+ | \`kb_ontology\` | 온톨로지 스키마 조회 |
1809
+ | \`kb_digest\` | 봇 구독 도메인 변경 다이제스트 |
1947
1810
 
1948
1811
  ---
1949
1812
 
1950
- ## 설치된 구성
1951
-
1952
- \`\`\`
1953
- .claude/
1954
- ├── CLAUDE.md # 이 파일
1955
- ├── settings.json # MCP 서버 설정 + SessionStart/Stop 훅
1956
- ├── memory/ # Core DB → 로컬 자동 동기화 컨텍스트
1957
- ├── skills/ # SEMO 스킬 (semo-system/semo-skills/ 링크)
1958
- ├── agents/ # SEMO 에이전트 (semo-system/meta/agents/ 링크)
1959
- └── commands/SEMO # 슬래시 커맨드 (semo-system/semo-core/commands/)
1960
- \`\`\`
1961
-
1962
- ---
1963
-
1964
- ## 프로젝트 규칙 (팀이 채워야 함)
1965
-
1966
- > 아래 섹션은 이 프로젝트 고유의 규칙을 기록하세요.
1967
- > 팀 공통 규칙은 \`memory/process.md\`에 있습니다.
1968
-
1969
- ### 기술 스택
1970
-
1971
- <!-- 예: Next.js 14, PostgreSQL, TypeScript strict mode -->
1813
+ ## KB-First 행동 규칙 (NON-NEGOTIABLE)
1972
1814
 
1973
- ### 브랜치 전략
1815
+ > KB는 팀의 Single Source of Truth이다. 아래 규칙은 예외 없이 적용된다.
1974
1816
 
1975
- <!-- 예: main(prod) / dev(staging) / feat/* -->
1817
+ ### 읽기 (Query-First)
1818
+ 다음 주제 질문 → **반드시 kb_search/kb_get으로 KB 먼저 조회** 후 답변:
1819
+ - 팀원 정보 → \`domain: team\`
1820
+ - 프로젝트 현황 → \`domain: project\`
1821
+ - 의사결정 기록 → \`domain: decision\`
1822
+ - 업무 프로세스 → \`domain: process\`
1823
+ - 인프라 구성 → \`domain: infra\`
1824
+ - KPI → \`domain: kpi\`
1976
1825
 
1977
- ### 코딩 컨벤션
1826
+ **금지:** 주제를 자체 지식/세션 기억만으로 답변하는 것.
1827
+ KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
1978
1828
 
1979
- <!-- 예: ESLint airbnb, 함수형 컴포넌트 필수, any 금지 -->
1829
+ ### 쓰기 (Write-Back)
1830
+ 다음 상황 → **반드시 kb_upsert로 KB에 즉시 기록:**
1831
+ - 사용자가 팀 정보를 정정하거나 새 사실을 알려줄 때
1832
+ - 의사결정이 내려졌을 때
1833
+ - 프로세스/규칙이 변경되었을 때
1980
1834
 
1981
- ### 아키텍처 특이사항
1982
-
1983
- <!-- 예: DB 직접 접근 금지 — 반드시 API route 통해야 함 -->
1835
+ **금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
1984
1836
 
1985
1837
  ---
1986
1838
 
1987
- ## Quality Gate
1839
+ ## Pre-Commit Quality Gate
1988
1840
 
1989
- 코드 변경 커밋 전 필수:
1841
+ > **코드 변경이 포함된 커밋 전 반드시 Quality Gate를 통과해야 합니다.**
1990
1842
 
1991
1843
  \`\`\`bash
1992
- npm run lint # ESLint
1993
- npx tsc --noEmit # TypeScript
1994
- npm run build # 빌드 검증
1844
+ npm run lint # ESLint 검사
1845
+ npx tsc --noEmit # TypeScript 타입 체크
1846
+ npm run build # 빌드 검증
1995
1847
  \`\`\`
1996
1848
 
1997
- \`--no-verify\` 사용 금지.
1849
+ \`--no-verify\` 플래그 사용 금지.
1998
1850
 
1999
1851
  ---
2000
1852
 
2001
- ## 슬래시 커맨드
1853
+ ## 환경변수 (\`~/.semo.env\`)
2002
1854
 
2003
- | 커맨드 | 설명 |
2004
- |--------|------|
2005
- | \`/SEMO:help\` | 도움말 |
2006
- | \`/SEMO:feedback\` | 피드백 제출 |
2007
- | \`/SEMO:health\` | 환경 헬스체크 |
1855
+ SEMO는 \`~/.semo.env\` 파일에서 공통 환경변수를 로드합니다.
1856
+ SessionStart 훅과 OpenClaw 게이트웨이 래퍼에서 자동 source됩니다.
2008
1857
 
2009
- ---
1858
+ | 변수 | 용도 | 필수 |
1859
+ |------|------|------|
1860
+ | \`DATABASE_URL\` | 팀 Core DB (PostgreSQL) 연결 | ✅ |
1861
+ | \`OPENAI_API_KEY\` | KB 임베딩용 (text-embedding-3-small) | 선택 |
1862
+ | \`SLACK_WEBHOOK\` | Slack 알림 | 선택 |
2010
1863
 
2011
- ## 복구 명령어
1864
+ 갱신이 필요하면 \`~/.semo.env\`를 직접 편집하거나 \`semo onboarding -f\`를 실행하세요.
2012
1865
 
2013
- \`\`\`bash
2014
- semo doctor # 환경 진단 (DB 연결, 설치 상태)
2015
- semo config db # DB URL 재설정
2016
- semo context sync # memory/ 수동 최신화
2017
- semo bots status # 봇 상태 조회
2018
- \`\`\`
1866
+ ---
1867
+
1868
+ > Generated by SEMO CLI v${VERSION}
2019
1869
  `;
2020
1870
  fs.writeFileSync(claudeMdPath, claudeMdContent);
2021
1871
  console.log(chalk_1.default.green("✓ .claude/CLAUDE.md 생성됨"));
@@ -2030,7 +1880,7 @@ program
2030
1880
  console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록\n"));
2031
1881
  // Standard 패키지 표시
2032
1882
  console.log(chalk_1.default.white.bold("Standard (필수)"));
2033
- const standardPkgs = ["semo-core", "semo-skills", "semo-agents", "semo-scripts"];
1883
+ const standardPkgs = ["semo-core", "semo-agents", "semo-scripts"];
2034
1884
  for (const pkg of standardPkgs) {
2035
1885
  const isInstalled = fs.existsSync(path.join(semoSystemDir, pkg));
2036
1886
  console.log(` ${isInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} ${pkg}`);
@@ -2063,7 +1913,6 @@ program
2063
1913
  console.log(chalk_1.default.white.bold("Standard:"));
2064
1914
  const standardChecks = [
2065
1915
  { name: "semo-core", path: path.join(semoSystemDir, "semo-core") },
2066
- { name: "semo-skills", path: path.join(semoSystemDir, "semo-skills") },
2067
1916
  ];
2068
1917
  let standardOk = true;
2069
1918
  for (const check of standardChecks) {
@@ -2101,11 +1950,27 @@ program
2101
1950
  .command("update")
2102
1951
  .description("SEMO를 최신 버전으로 업데이트합니다")
2103
1952
  .option("--self", "CLI만 업데이트")
1953
+ .option("--global", "글로벌 스킬/커맨드/에이전트를 DB 최신으로 갱신 (~/.claude/)")
2104
1954
  .option("--system", "semo-system만 업데이트")
2105
1955
  .option("--skip-cli", "CLI 업데이트 건너뛰기")
2106
- .option("--only <packages>", "특정 패키지만 업데이트 (쉼표 구분: semo-core,semo-skills,biz/management)")
1956
+ .option("--only <packages>", "특정 패키지만 업데이트 (쉼표 구분: semo-core,biz/management)")
2107
1957
  .option("--migrate", "레거시 환경 강제 마이그레이션")
2108
1958
  .action(async (options) => {
1959
+ // === --global: 글로벌 스킬 갱신 ===
1960
+ if (options.global) {
1961
+ console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 글로벌 업데이트\n"));
1962
+ console.log(chalk_1.default.gray(" 대상: ~/.claude/skills, commands, agents (DB 최신)\n"));
1963
+ const connected = await (0, database_1.isDbConnected)();
1964
+ if (!connected) {
1965
+ console.log(chalk_1.default.red(" DB 연결 실패 — ~/.semo.env를 확인하세요."));
1966
+ await (0, database_1.closeConnection)();
1967
+ process.exit(1);
1968
+ }
1969
+ await setupStandardGlobal();
1970
+ await (0, database_1.closeConnection)();
1971
+ console.log(chalk_1.default.green.bold("\n✅ 글로벌 스킬 업데이트 완료!\n"));
1972
+ return;
1973
+ }
2109
1974
  console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 업데이트\n"));
2110
1975
  const cwd = process.cwd();
2111
1976
  const semoSystemDir = path.join(cwd, "semo-system");
@@ -2159,22 +2024,19 @@ program
2159
2024
  }
2160
2025
  // 업데이트 대상 결정
2161
2026
  const updateSemoCore = !isSelectiveUpdate || onlyPackages.includes("semo-core");
2162
- const updateSemoSkills = !isSelectiveUpdate || onlyPackages.includes("semo-skills");
2163
2027
  const updateSemoAgents = !isSelectiveUpdate || onlyPackages.includes("semo-agents");
2164
2028
  const updateSemoScripts = !isSelectiveUpdate || onlyPackages.includes("semo-scripts");
2165
2029
  console.log(chalk_1.default.cyan("\n📚 semo-system 업데이트"));
2166
2030
  console.log(chalk_1.default.gray(" 대상:"));
2167
2031
  if (updateSemoCore)
2168
2032
  console.log(chalk_1.default.gray(" - semo-core"));
2169
- if (updateSemoSkills)
2170
- console.log(chalk_1.default.gray(" - semo-skills"));
2171
2033
  if (updateSemoAgents)
2172
2034
  console.log(chalk_1.default.gray(" - semo-agents"));
2173
2035
  if (updateSemoScripts)
2174
2036
  console.log(chalk_1.default.gray(" - semo-scripts"));
2175
- if (!updateSemoCore && !updateSemoSkills && !updateSemoAgents && !updateSemoScripts) {
2037
+ if (!updateSemoCore && !updateSemoAgents && !updateSemoScripts) {
2176
2038
  console.log(chalk_1.default.yellow("\n ⚠️ 업데이트할 패키지가 없습니다."));
2177
- console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-skills, semo-agents, semo-scripts"));
2039
+ console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-agents, semo-scripts"));
2178
2040
  return;
2179
2041
  }
2180
2042
  const spinner = (0, ora_1.default)("\n 최신 버전 다운로드 중...").start();
@@ -2185,7 +2047,6 @@ program
2185
2047
  // Standard 업데이트 (선택적) - semo-system/ 하위에서 복사
2186
2048
  const standardUpdates = [
2187
2049
  { flag: updateSemoCore, name: "semo-core" },
2188
- { flag: updateSemoSkills, name: "semo-skills" },
2189
2050
  { flag: updateSemoAgents, name: "semo-agents" },
2190
2051
  { flag: updateSemoScripts, name: "semo-scripts" },
2191
2052
  ];
@@ -2272,7 +2133,7 @@ program
2272
2133
  }
2273
2134
  }
2274
2135
  // === 6. Hooks 업데이트 ===
2275
- await setupHooks(cwd, true);
2136
+ await setupHooks(true);
2276
2137
  // === 7. 설치 검증 ===
2277
2138
  const verificationResult = verifyInstallation(cwd, []);
2278
2139
  printVerificationResult(verificationResult);
@@ -2331,20 +2192,22 @@ program
2331
2192
  // === config 명령어 (설치 후 설정 변경) ===
2332
2193
  const configCmd = program.command("config").description("SEMO 설정 관리");
2333
2194
  configCmd
2334
- .command("db")
2335
- .description(" Core DB 접속정보 설정 (DATABASE_URL ~/.semo.env)")
2195
+ .command("env")
2196
+ .description("SEMO 환경변수 설정 (DATABASE_URL, OPENAI_API_KEY )")
2336
2197
  .option("--credentials-gist <gistId>", "Private GitHub Gist에서 자동 가져오기")
2198
+ .option("--force", "기존 값도 Gist에서 덮어쓰기")
2337
2199
  .action(async (options) => {
2338
- console.log(chalk_1.default.cyan.bold("\n🔑 DB 접속정보 설정\n"));
2339
- // 기존 확인
2340
- const existing = readSemoEnvDbUrl();
2341
- if (existing) {
2200
+ console.log(chalk_1.default.cyan.bold("\n🔑 SEMO 환경변수 설정\n"));
2201
+ const existing = readSemoEnvCreds();
2202
+ const hasExisting = Object.keys(existing).length > 0;
2203
+ if (hasExisting && !options.force) {
2204
+ const keys = Object.keys(existing).join(", ");
2342
2205
  const { overwrite } = await inquirer_1.default.prompt([
2343
2206
  {
2344
2207
  type: "confirm",
2345
2208
  name: "overwrite",
2346
- message: `기존 DATABASE_URL이 있습니다. 덮어쓰시겠습니까?`,
2347
- default: false,
2209
+ message: `기존 설정이 있습니다 (${keys}). Gist에서 없는 키를 보충하시겠습니까?`,
2210
+ default: true,
2348
2211
  },
2349
2212
  ]);
2350
2213
  if (!overwrite) {
@@ -2352,34 +2215,8 @@ configCmd
2352
2215
  return;
2353
2216
  }
2354
2217
  }
2355
- // Gist 또는 프롬프트로 가져오기
2356
- const gistId = options.credentialsGist || process.env.SEMO_CREDENTIALS_GIST;
2357
- if (gistId) {
2358
- console.log(chalk_1.default.gray("GitHub Gist에서 접속정보 가져오는 중..."));
2359
- const url = fetchDbUrlFromGist(gistId);
2360
- if (url) {
2361
- writeSemoEnvFile(url);
2362
- console.log(chalk_1.default.green("✅ ~/.semo.env 업데이트 완료"));
2363
- return;
2364
- }
2365
- console.log(chalk_1.default.yellow("⚠️ Gist fetch 실패 — 수동 입력"));
2366
- }
2367
- const { dbUrl } = await inquirer_1.default.prompt([
2368
- {
2369
- type: "password",
2370
- name: "dbUrl",
2371
- message: "DATABASE_URL:",
2372
- mask: "*",
2373
- },
2374
- ]);
2375
- if (dbUrl && dbUrl.trim()) {
2376
- writeSemoEnvFile(dbUrl.trim());
2377
- console.log(chalk_1.default.green("✅ ~/.semo.env 저장됨 (권한: 600)"));
2378
- console.log(chalk_1.default.gray(" 다음 Claude Code 세션부터 자동으로 적용됩니다."));
2379
- }
2380
- else {
2381
- console.log(chalk_1.default.yellow("취소됨"));
2382
- }
2218
+ await setupSemoEnv(options.credentialsGist, options.force);
2219
+ console.log(chalk_1.default.gray(" 다음 Claude Code 세션부터 자동으로 적용됩니다."));
2383
2220
  });
2384
2221
  // === doctor 명령어 (설치 상태 진단) ===
2385
2222
  program
@@ -2410,7 +2247,7 @@ program
2410
2247
  console.log(chalk_1.default.gray(" 💡 해결: semo init 실행"));
2411
2248
  }
2412
2249
  else {
2413
- const packages = ["semo-core", "semo-skills", "semo-agents", "semo-scripts"];
2250
+ const packages = ["semo-core", "semo-agents", "semo-scripts"];
2414
2251
  for (const pkg of packages) {
2415
2252
  const pkgPath = path.join(semoSystemDir, pkg);
2416
2253
  if (fs.existsSync(pkgPath)) {
@@ -2484,19 +2321,7 @@ function readSyncState(cwd) {
2484
2321
  }
2485
2322
  catch { /* */ }
2486
2323
  }
2487
- return { botId: "", lastPull: null, lastPush: null, sharedCount: 0, botCount: 0 };
2488
- }
2489
- function detectBotId() {
2490
- // 1. Env var
2491
- if (process.env.SEMO_BOT_ID)
2492
- return process.env.SEMO_BOT_ID;
2493
- // 2. Detect from cwd (e.g. ~/.openclaw-workclaw → workclaw)
2494
- const cwd = process.cwd();
2495
- const match = cwd.match(/\.openclaw-(\w+)/);
2496
- if (match)
2497
- return match[1];
2498
- // 3. Default
2499
- return "unknown";
2324
+ return { lastPull: null, lastPush: null, sharedCount: 0 };
2500
2325
  }
2501
2326
  const kbCmd = program
2502
2327
  .command("kb")
@@ -2504,16 +2329,14 @@ const kbCmd = program
2504
2329
  kbCmd
2505
2330
  .command("pull")
2506
2331
  .description("DB에서 KB를 로컬 .kb/로 내려받기")
2507
- .option("--bot <name>", "봇 ID", detectBotId())
2508
2332
  .option("--domain <name>", "특정 도메인만")
2509
2333
  .action(async (options) => {
2510
2334
  const spinner = (0, ora_1.default)("KB 데이터 가져오는 중...").start();
2511
2335
  try {
2512
2336
  const pool = (0, database_1.getPool)();
2513
- const result = await (0, kb_1.kbPull)(pool, options.bot, options.domain, process.cwd());
2337
+ const result = await (0, kb_1.kbPull)(pool, options.domain, process.cwd());
2514
2338
  spinner.succeed(`KB pull 완료`);
2515
- console.log(chalk_1.default.green(` 📦 공통 KB: ${result.shared.length}건`));
2516
- console.log(chalk_1.default.green(` 🤖 봇 KB (${options.bot}): ${result.bot.length}건`));
2339
+ console.log(chalk_1.default.green(` 📦 KB: ${result.length}건`));
2517
2340
  // Also pull ontology
2518
2341
  const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
2519
2342
  console.log(chalk_1.default.green(` 📐 온톨로지: ${ontoCount}개 도메인`));
@@ -2529,9 +2352,8 @@ kbCmd
2529
2352
  kbCmd
2530
2353
  .command("push")
2531
2354
  .description("로컬 .kb/ 데이터를 DB에 업로드")
2532
- .option("--bot <name>", " ID", detectBotId())
2533
- .option("--target <type>", "대상 (shared|bot)", "bot")
2534
- .option("--file <path>", ".kb/ 내 특정 파일")
2355
+ .option("--file <path>", ".kb/ 내 특정 파일", "team.json")
2356
+ .option("--created-by <name>", "작성자 식별자")
2535
2357
  .action(async (options) => {
2536
2358
  const cwd = process.cwd();
2537
2359
  const kbDir = path.join(cwd, ".kb");
@@ -2542,14 +2364,13 @@ kbCmd
2542
2364
  const spinner = (0, ora_1.default)("KB 데이터 업로드 중...").start();
2543
2365
  try {
2544
2366
  const pool = (0, database_1.getPool)();
2545
- const filename = options.file || (options.target === "shared" ? "team.json" : "bot.json");
2546
- const filePath = path.join(kbDir, filename);
2367
+ const filePath = path.join(kbDir, options.file);
2547
2368
  if (!fs.existsSync(filePath)) {
2548
- spinner.fail(`파일을 찾을 수 없습니다: .kb/${filename}`);
2369
+ spinner.fail(`파일을 찾을 수 없습니다: .kb/${options.file}`);
2549
2370
  process.exit(1);
2550
2371
  }
2551
2372
  const entries = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2552
- const result = await (0, kb_1.kbPush)(pool, options.bot, entries, options.target, cwd);
2373
+ const result = await (0, kb_1.kbPush)(pool, entries, options.createdBy, cwd);
2553
2374
  spinner.succeed(`KB push 완료`);
2554
2375
  console.log(chalk_1.default.green(` ✅ ${result.upserted}건 업서트됨`));
2555
2376
  if (result.errors.length > 0) {
@@ -2567,15 +2388,14 @@ kbCmd
2567
2388
  kbCmd
2568
2389
  .command("status")
2569
2390
  .description("KB 동기화 상태 확인")
2570
- .option("--bot <name>", "봇 ID", detectBotId())
2571
- .action(async (options) => {
2391
+ .action(async () => {
2572
2392
  const spinner = (0, ora_1.default)("KB 상태 조회 중...").start();
2573
2393
  try {
2574
2394
  const pool = (0, database_1.getPool)();
2575
- const status = await (0, kb_1.kbStatus)(pool, options.bot);
2395
+ const status = await (0, kb_1.kbStatus)(pool);
2576
2396
  spinner.stop();
2577
2397
  console.log(chalk_1.default.cyan.bold("\n📊 KB 상태\n"));
2578
- console.log(chalk_1.default.white(" 📦 공통 KB (shared)"));
2398
+ console.log(chalk_1.default.white(" 📦 KB (knowledge_base)"));
2579
2399
  console.log(chalk_1.default.gray(` 총 ${status.shared.total}건`));
2580
2400
  for (const [domain, count] of Object.entries(status.shared.domains)) {
2581
2401
  console.log(chalk_1.default.gray(` - ${domain}: ${count}건`));
@@ -2583,17 +2403,6 @@ kbCmd
2583
2403
  if (status.shared.lastUpdated) {
2584
2404
  console.log(chalk_1.default.gray(` 최종 업데이트: ${status.shared.lastUpdated}`));
2585
2405
  }
2586
- console.log(chalk_1.default.white(`\n 🤖 봇 KB (${options.bot})`));
2587
- console.log(chalk_1.default.gray(` 총 ${status.bot.total}건`));
2588
- for (const [domain, count] of Object.entries(status.bot.domains)) {
2589
- console.log(chalk_1.default.gray(` - ${domain}: ${count}건`));
2590
- }
2591
- if (status.bot.lastUpdated) {
2592
- console.log(chalk_1.default.gray(` 최종 업데이트: ${status.bot.lastUpdated}`));
2593
- }
2594
- if (status.bot.lastSynced) {
2595
- console.log(chalk_1.default.gray(` 최종 동기화: ${status.bot.lastSynced}`));
2596
- }
2597
2406
  // Local sync state
2598
2407
  const syncState = readSyncState(process.cwd());
2599
2408
  if (syncState.lastPull || syncState.lastPush) {
@@ -2615,42 +2424,32 @@ kbCmd
2615
2424
  kbCmd
2616
2425
  .command("list")
2617
2426
  .description("KB 항목 목록 조회")
2618
- .option("--bot <name>", "봇 ID", detectBotId())
2619
2427
  .option("--domain <name>", "도메인 필터")
2428
+ .option("--service <name>", "서비스(프로젝트) 필터 — 해당 서비스의 모든 도메인 항목 반환")
2620
2429
  .option("--limit <n>", "최대 항목 수", "50")
2621
2430
  .option("--format <type>", "출력 형식 (table|json)", "table")
2622
2431
  .action(async (options) => {
2623
2432
  try {
2624
2433
  const pool = (0, database_1.getPool)();
2625
- const result = await (0, kb_1.kbList)(pool, {
2434
+ const entries = await (0, kb_1.kbList)(pool, {
2626
2435
  domain: options.domain,
2627
- botId: options.bot,
2436
+ service: options.service,
2628
2437
  limit: parseInt(options.limit),
2629
2438
  });
2630
2439
  if (options.format === "json") {
2631
- console.log(JSON.stringify(result, null, 2));
2440
+ console.log(JSON.stringify(entries, null, 2));
2632
2441
  }
2633
2442
  else {
2634
2443
  console.log(chalk_1.default.cyan.bold("\n📋 KB 목록\n"));
2635
- if (result.shared.length > 0) {
2636
- console.log(chalk_1.default.white(" 📦 공통 KB"));
2444
+ if (entries.length > 0) {
2637
2445
  console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
2638
- for (const entry of result.shared) {
2446
+ for (const entry of entries) {
2639
2447
  const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
2640
2448
  console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
2641
2449
  console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
2642
2450
  }
2643
2451
  }
2644
- if (result.bot.length > 0) {
2645
- console.log(chalk_1.default.white(`\n 🤖 봇 KB (${options.bot})`));
2646
- console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
2647
- for (const entry of result.bot) {
2648
- const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
2649
- console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
2650
- console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
2651
- }
2652
- }
2653
- if (result.shared.length === 0 && result.bot.length === 0) {
2452
+ else {
2654
2453
  console.log(chalk_1.default.yellow(" KB가 비어있습니다."));
2655
2454
  }
2656
2455
  console.log();
@@ -2666,8 +2465,8 @@ kbCmd
2666
2465
  kbCmd
2667
2466
  .command("search <query>")
2668
2467
  .description("KB 검색 (시맨틱 + 텍스트 하이브리드)")
2669
- .option("--bot <name>", "봇 KB도 검색", detectBotId())
2670
2468
  .option("--domain <name>", "도메인 필터")
2469
+ .option("--service <name>", "서비스(프로젝트) 필터")
2671
2470
  .option("--limit <n>", "최대 결과 수", "10")
2672
2471
  .option("--mode <type>", "검색 모드 (hybrid|semantic|text)", "hybrid")
2673
2472
  .action(async (query, options) => {
@@ -2676,7 +2475,7 @@ kbCmd
2676
2475
  const pool = (0, database_1.getPool)();
2677
2476
  const results = await (0, kb_1.kbSearch)(pool, query, {
2678
2477
  domain: options.domain,
2679
- botId: options.bot,
2478
+ service: options.service,
2680
2479
  limit: parseInt(options.limit),
2681
2480
  mode: options.mode,
2682
2481
  });
@@ -2703,46 +2502,9 @@ kbCmd
2703
2502
  process.exit(1);
2704
2503
  }
2705
2504
  });
2706
- kbCmd
2707
- .command("diff")
2708
- .description("로컬 .kb/ vs DB 차이 비교")
2709
- .option("--bot <name>", "봇 ID", detectBotId())
2710
- .action(async (options) => {
2711
- const spinner = (0, ora_1.default)("차이 비교 중...").start();
2712
- try {
2713
- const pool = (0, database_1.getPool)();
2714
- const diff = await (0, kb_1.kbDiff)(pool, options.bot, process.cwd());
2715
- spinner.stop();
2716
- console.log(chalk_1.default.cyan.bold("\n📊 KB Diff\n"));
2717
- console.log(chalk_1.default.green(` ✚ 추가됨 (DB에만 있음): ${diff.added.length}건`));
2718
- console.log(chalk_1.default.red(` ✖ 삭제됨 (로컬에만 있음): ${diff.removed.length}건`));
2719
- console.log(chalk_1.default.yellow(` ✎ 변경됨: ${diff.modified.length}건`));
2720
- console.log(chalk_1.default.gray(` ═ 동일: ${diff.unchanged}건`));
2721
- if (diff.added.length > 0) {
2722
- console.log(chalk_1.default.green("\n ✚ 추가된 항목:"));
2723
- diff.added.forEach(e => console.log(chalk_1.default.gray(` [${e.domain}] ${e.key}`)));
2724
- }
2725
- if (diff.removed.length > 0) {
2726
- console.log(chalk_1.default.red("\n ✖ 삭제된 항목:"));
2727
- diff.removed.forEach(e => console.log(chalk_1.default.gray(` [${e.domain}] ${e.key}`)));
2728
- }
2729
- if (diff.modified.length > 0) {
2730
- console.log(chalk_1.default.yellow("\n ✎ 변경된 항목:"));
2731
- diff.modified.forEach(m => console.log(chalk_1.default.gray(` [${m.local.domain}] ${m.local.key}`)));
2732
- }
2733
- console.log();
2734
- await (0, database_1.closeConnection)();
2735
- }
2736
- catch (err) {
2737
- spinner.fail(`Diff 실패: ${err}`);
2738
- await (0, database_1.closeConnection)();
2739
- process.exit(1);
2740
- }
2741
- });
2742
2505
  kbCmd
2743
2506
  .command("embed")
2744
2507
  .description("기존 KB 항목에 임베딩 벡터 생성 (OPENAI_API_KEY 필요)")
2745
- .option("--bot <name>", "봇 KB도 임베딩", detectBotId())
2746
2508
  .option("--domain <name>", "도메인 필터")
2747
2509
  .option("--force", "이미 임베딩된 항목도 재생성")
2748
2510
  .action(async (options) => {
@@ -2755,29 +2517,17 @@ kbCmd
2755
2517
  try {
2756
2518
  const pool = (0, database_1.getPool)();
2757
2519
  const client = await pool.connect();
2758
- // Shared KB
2759
- let sharedSql = "SELECT kb_id, domain, key, content FROM semo.knowledge_base WHERE 1=1";
2760
- const sharedParams = [];
2520
+ let sql = "SELECT kb_id, domain, key, content FROM semo.knowledge_base WHERE 1=1";
2521
+ const params = [];
2761
2522
  let pIdx = 1;
2762
2523
  if (!options.force)
2763
- sharedSql += " AND embedding IS NULL";
2524
+ sql += " AND embedding IS NULL";
2764
2525
  if (options.domain) {
2765
- sharedSql += ` AND domain = $${pIdx++}`;
2766
- sharedParams.push(options.domain);
2767
- }
2768
- const sharedRows = await client.query(sharedSql, sharedParams);
2769
- // Bot KB
2770
- let botSql = "SELECT id, domain, key, content FROM semo.bot_knowledge WHERE bot_id = $1";
2771
- const botParams = [options.bot];
2772
- let bIdx = 2;
2773
- if (!options.force)
2774
- botSql += " AND embedding IS NULL";
2775
- if (options.domain) {
2776
- botSql += ` AND domain = $${bIdx++}`;
2777
- botParams.push(options.domain);
2526
+ sql += ` AND domain = $${pIdx++}`;
2527
+ params.push(options.domain);
2778
2528
  }
2779
- const botRows = await client.query(botSql, botParams);
2780
- const total = sharedRows.rows.length + botRows.rows.length;
2529
+ const rows = await client.query(sql, params);
2530
+ const total = rows.rows.length;
2781
2531
  spinner.succeed(`${total}건 임베딩 대상`);
2782
2532
  if (total === 0) {
2783
2533
  console.log(chalk_1.default.green(" 모든 항목이 이미 임베딩되어 있습니다."));
@@ -2787,8 +2537,7 @@ kbCmd
2787
2537
  }
2788
2538
  let done = 0;
2789
2539
  const embedSpinner = (0, ora_1.default)(`임베딩 생성 중... 0/${total}`).start();
2790
- // Process shared KB
2791
- for (const row of sharedRows.rows) {
2540
+ for (const row of rows.rows) {
2792
2541
  const embedding = await (0, kb_1.generateEmbedding)(`${row.key}: ${row.content}`);
2793
2542
  if (embedding) {
2794
2543
  await client.query("UPDATE semo.knowledge_base SET embedding = $1::vector WHERE kb_id = $2", [`[${embedding.join(",")}]`, row.kb_id]);
@@ -2796,15 +2545,6 @@ kbCmd
2796
2545
  done++;
2797
2546
  embedSpinner.text = `임베딩 생성 중... ${done}/${total}`;
2798
2547
  }
2799
- // Process bot KB
2800
- for (const row of botRows.rows) {
2801
- const embedding = await (0, kb_1.generateEmbedding)(`${row.key}: ${row.content}`);
2802
- if (embedding) {
2803
- await client.query("UPDATE semo.bot_knowledge SET embedding = $1::vector WHERE id = $2", [`[${embedding.join(",")}]`, row.id]);
2804
- }
2805
- done++;
2806
- embedSpinner.text = `임베딩 생성 중... ${done}/${total}`;
2807
- }
2808
2548
  embedSpinner.succeed(`${done}건 임베딩 완료`);
2809
2549
  client.release();
2810
2550
  await (0, database_1.closeConnection)();
@@ -2818,7 +2558,6 @@ kbCmd
2818
2558
  kbCmd
2819
2559
  .command("sync")
2820
2560
  .description("양방향 동기화 (pull → merge → push)")
2821
- .option("--bot <name>", "봇 ID", detectBotId())
2822
2561
  .option("--domain <name>", "도메인 필터")
2823
2562
  .action(async (options) => {
2824
2563
  console.log(chalk_1.default.cyan.bold("\n🔄 KB 동기화\n"));
@@ -2826,8 +2565,8 @@ kbCmd
2826
2565
  try {
2827
2566
  const pool = (0, database_1.getPool)();
2828
2567
  // Step 1: Pull
2829
- const pulled = await (0, kb_1.kbPull)(pool, options.bot, options.domain, process.cwd());
2830
- spinner.succeed(`Pull 완료: 공통 ${pulled.shared.length}건, 봇 ${pulled.bot.length}건`);
2568
+ const pulled = await (0, kb_1.kbPull)(pool, options.domain, process.cwd());
2569
+ spinner.succeed(`Pull 완료: ${pulled.length}건`);
2831
2570
  // Step 2: Pull ontology
2832
2571
  const spinner2 = (0, ora_1.default)("Step 2/2: 온톨로지 동기화...").start();
2833
2572
  const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
@@ -2850,11 +2589,15 @@ const ontoCmd = program
2850
2589
  ontoCmd
2851
2590
  .command("list")
2852
2591
  .description("정의된 온톨로지 도메인 목록")
2592
+ .option("--service <name>", "서비스별 필터")
2853
2593
  .option("--format <type>", "출력 형식 (table|json)", "table")
2854
2594
  .action(async (options) => {
2855
2595
  try {
2856
2596
  const pool = (0, database_1.getPool)();
2857
- const domains = await (0, kb_1.ontoList)(pool);
2597
+ let domains = await (0, kb_1.ontoList)(pool);
2598
+ if (options.service) {
2599
+ domains = domains.filter(d => d.service === options.service || d.domain === options.service || d.domain.startsWith(`${options.service}.`));
2600
+ }
2858
2601
  if (options.format === "json") {
2859
2602
  console.log(JSON.stringify(domains, null, 2));
2860
2603
  }
@@ -2864,10 +2607,66 @@ ontoCmd
2864
2607
  console.log(chalk_1.default.yellow(" 온톨로지가 정의되지 않았습니다."));
2865
2608
  }
2866
2609
  else {
2610
+ // Group by service
2611
+ const global = domains.filter(d => !d.service);
2612
+ const byService = {};
2867
2613
  for (const d of domains) {
2868
- console.log(chalk_1.default.cyan(` ${d.domain}`) + chalk_1.default.gray(` (v${d.version})`));
2869
- if (d.description)
2870
- console.log(chalk_1.default.gray(` ${d.description}`));
2614
+ if (d.service) {
2615
+ if (!byService[d.service])
2616
+ byService[d.service] = [];
2617
+ byService[d.service].push(d);
2618
+ }
2619
+ }
2620
+ if (global.length > 0) {
2621
+ console.log(chalk_1.default.white.bold(" Global"));
2622
+ for (const d of global) {
2623
+ const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
2624
+ console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + chalk_1.default.gray(` (v${d.version})`));
2625
+ if (d.description)
2626
+ console.log(chalk_1.default.gray(` ${d.description}`));
2627
+ }
2628
+ }
2629
+ for (const [svc, svcDomains] of Object.entries(byService)) {
2630
+ console.log(chalk_1.default.white.bold(`\n Service: ${svc}`));
2631
+ for (const d of svcDomains) {
2632
+ const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
2633
+ console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + chalk_1.default.gray(` (v${d.version})`));
2634
+ if (d.description)
2635
+ console.log(chalk_1.default.gray(` ${d.description}`));
2636
+ }
2637
+ }
2638
+ }
2639
+ console.log();
2640
+ }
2641
+ await (0, database_1.closeConnection)();
2642
+ }
2643
+ catch (err) {
2644
+ console.error(chalk_1.default.red(`조회 실패: ${err}`));
2645
+ await (0, database_1.closeConnection)();
2646
+ process.exit(1);
2647
+ }
2648
+ });
2649
+ ontoCmd
2650
+ .command("types")
2651
+ .description("온톨로지 타입 목록 (구조적 템플릿)")
2652
+ .option("--format <type>", "출력 형식 (table|json)", "table")
2653
+ .action(async (options) => {
2654
+ try {
2655
+ const pool = (0, database_1.getPool)();
2656
+ const types = await (0, kb_1.ontoListTypes)(pool);
2657
+ if (options.format === "json") {
2658
+ console.log(JSON.stringify(types, null, 2));
2659
+ }
2660
+ else {
2661
+ console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 타입\n"));
2662
+ if (types.length === 0) {
2663
+ console.log(chalk_1.default.yellow(" 타입이 정의되지 않았습니다. (016 마이그레이션 실행 필요)"));
2664
+ }
2665
+ else {
2666
+ for (const t of types) {
2667
+ console.log(chalk_1.default.cyan(` ${t.type_key}`) + chalk_1.default.gray(` (v${t.version})`));
2668
+ if (t.description)
2669
+ console.log(chalk_1.default.gray(` ${t.description}`));
2871
2670
  }
2872
2671
  }
2873
2672
  console.log();
@@ -2957,6 +2756,8 @@ ontoCmd
2957
2756
  (0, bots_1.registerBotsCommands)(program);
2958
2757
  (0, get_1.registerGetCommands)(program);
2959
2758
  (0, sessions_1.registerSessionsCommands)(program);
2759
+ (0, db_1.registerDbCommands)(program);
2760
+ (0, memory_1.registerMemoryCommands)(program);
2960
2761
  // === semo skills — DB 시딩 ===
2961
2762
  /**
2962
2763
  * SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
@@ -2984,119 +2785,12 @@ function parseSkillFrontmatter(content) {
2984
2785
  // tools 배열
2985
2786
  const toolsMatch = fm.match(/^tools:\s*\[(.+)\]$/m);
2986
2787
  const tools = toolsMatch ? toolsMatch[1].split(",").map((t) => t.trim()) : [];
2987
- // category: 상위 디렉토리명 또는 name 자체 (semo-skills의 경우 dir = skill name)
2788
+ // category
2988
2789
  const category = "core";
2989
2790
  return { name, description, category, tools };
2990
2791
  }
2991
- /**
2992
- * semo-system/semo-skills/ 스캔 semo.skills 테이블 upsert
2993
- */
2994
- async function seedSkillsToDb(semoSystemDir) {
2995
- const skillsDir = path.join(semoSystemDir, "semo-skills");
2996
- if (!fs.existsSync(skillsDir)) {
2997
- console.log(chalk_1.default.red(`\n❌ semo-skills 디렉토리를 찾을 수 없습니다: ${skillsDir}`));
2998
- return;
2999
- }
3000
- const connected = await (0, database_1.isDbConnected)();
3001
- if (!connected) {
3002
- console.log(chalk_1.default.red("❌ DB 연결 실패 — seed-skills 건너뜀"));
3003
- return;
3004
- }
3005
- const spinner = (0, ora_1.default)("semo-skills 스캔 중...").start();
3006
- // 활성 스킬 디렉토리 수집 (_archived, CHANGELOG, VERSION 제외)
3007
- const excludeDirs = new Set(["_archived", "CHANGELOG"]);
3008
- const excludeFiles = new Set(["VERSION"]);
3009
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
3010
- const skillDirs = entries.filter(e => e.isDirectory() && !excludeDirs.has(e.name));
3011
- const skills = [];
3012
- for (const dir of skillDirs) {
3013
- const skillMdPath = path.join(skillsDir, dir.name, "SKILL.md");
3014
- if (!fs.existsSync(skillMdPath))
3015
- continue;
3016
- const content = fs.readFileSync(skillMdPath, "utf-8");
3017
- const parsed = parseSkillFrontmatter(content);
3018
- if (!parsed)
3019
- continue;
3020
- skills.push({ ...parsed, content });
3021
- }
3022
- spinner.text = `${skills.length}개 스킬 발견 — DB에 upsert 중...`;
3023
- const pool = (0, database_1.getPool)();
3024
- const client = await pool.connect();
3025
- let upserted = 0;
3026
- const errors = [];
3027
- try {
3028
- await client.query("BEGIN");
3029
- for (let i = 0; i < skills.length; i++) {
3030
- const skill = skills[i];
3031
- try {
3032
- await client.query(`INSERT INTO semo.skills
3033
- (name, display_name, description, content, category, package,
3034
- is_active, is_required, install_order, version)
3035
- VALUES ($1, $2, $3, $4, $5, $6, true, false, $7, '1.0.0')
3036
- ON CONFLICT (name) DO UPDATE SET
3037
- display_name = EXCLUDED.display_name,
3038
- description = EXCLUDED.description,
3039
- content = EXCLUDED.content,
3040
- category = EXCLUDED.category`, [skill.name, skill.name, skill.description, skill.content, skill.category, "semo-skills", i + 1]);
3041
- upserted++;
3042
- }
3043
- catch (err) {
3044
- errors.push(`${skill.name}: ${err}`);
3045
- }
3046
- }
3047
- await client.query("COMMIT");
3048
- spinner.succeed(`seed-skills 완료: ${upserted}개 스킬 upsert`);
3049
- if (errors.length > 0) {
3050
- errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
3051
- }
3052
- }
3053
- catch (err) {
3054
- await client.query("ROLLBACK");
3055
- spinner.fail(`seed-skills 실패: ${err}`);
3056
- }
3057
- finally {
3058
- client.release();
3059
- }
3060
- }
3061
- // `semo skills` 커맨드 그룹
3062
- const skillsCmd = program
3063
- .command("skills")
3064
- .description("스킬 관리 (DB 시딩 등)");
3065
- skillsCmd
3066
- .command("seed")
3067
- .description("semo-system/semo-skills/ → semo.skills DB upsert")
3068
- .option("--semo-system <path>", "semo-system 경로 (기본: ./semo-system)")
3069
- .option("--dry-run", "실제 upsert 없이 스캔 결과만 출력")
3070
- .action(async (options) => {
3071
- const cwd = process.cwd();
3072
- const semoSystemDir = options.semoSystem
3073
- ? path.resolve(options.semoSystem)
3074
- : path.join(cwd, "semo-system");
3075
- if (options.dryRun) {
3076
- const skillsDir = path.join(semoSystemDir, "semo-skills");
3077
- if (!fs.existsSync(skillsDir)) {
3078
- console.log(chalk_1.default.red(`❌ ${skillsDir} 없음`));
3079
- process.exit(1);
3080
- }
3081
- const excludeDirs = new Set(["_archived", "CHANGELOG"]);
3082
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
3083
- const skillDirs = entries.filter(e => e.isDirectory() && !excludeDirs.has(e.name));
3084
- console.log(chalk_1.default.cyan.bold("\n[dry-run] 발견된 스킬:\n"));
3085
- for (const dir of skillDirs) {
3086
- const mdPath = path.join(skillsDir, dir.name, "SKILL.md");
3087
- if (!fs.existsSync(mdPath))
3088
- continue;
3089
- const parsed = parseSkillFrontmatter(fs.readFileSync(mdPath, "utf-8"));
3090
- if (parsed) {
3091
- console.log(chalk_1.default.gray(` ${parsed.name.padEnd(20)} ${parsed.description.split("\n")[0].substring(0, 60)}`));
3092
- }
3093
- }
3094
- console.log();
3095
- return;
3096
- }
3097
- await seedSkillsToDb(semoSystemDir);
3098
- await (0, database_1.closeConnection)();
3099
- });
2792
+ // semo skills seed — 제거됨 (중앙 DB 단일 SoT)
2793
+ // 전용 스킬은 semo bots seed로 동기화
3100
2794
  // === -v 옵션 처리 (program.parse 전에 직접 처리) ===
3101
2795
  async function main() {
3102
2796
  const args = process.argv.slice(2);