@team-semicolon/semo-cli 4.1.4 → 4.2.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,14 @@ 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 global_cache_1 = require("./global-cache");
64
67
  const PACKAGE_NAME = "@team-semicolon/semo-cli";
65
68
  // package.json에서 버전 동적 로드
66
69
  function getCliVersion() {
@@ -133,7 +136,7 @@ async function getRemotePackageVersion(packagePath) {
133
136
  }
134
137
  }
135
138
  /**
136
- * semo-core/semo-skills 원격 버전 가져오기 (semo-system/ 하위 경로)
139
+ * semo-core 원격 버전 가져오기 (semo-system/ 하위 경로)
137
140
  */
138
141
  async function getRemoteCoreVersion(type) {
139
142
  try {
@@ -159,7 +162,7 @@ async function showVersionComparison(cwd) {
159
162
  // 1. CLI 버전 비교
160
163
  const currentCliVersion = VERSION;
161
164
  const latestCliVersion = await getLatestVersion();
162
- // 2. semo-core, semo-skills 버전 비교
165
+ // 2. semo-core 버전 비교
163
166
  const semoSystemDir = path.join(cwd, "semo-system");
164
167
  const hasSemoSystem = fs.existsSync(semoSystemDir);
165
168
  const versionInfos = [];
@@ -171,15 +174,14 @@ async function showVersionComparison(cwd) {
171
174
  needsUpdate: latestCliVersion ? isVersionLower(currentCliVersion, latestCliVersion) : false,
172
175
  level: 0,
173
176
  });
174
- // 레거시 환경 경고 (루트에 semo-core/semo-skills가 있는 경우)
177
+ // 레거시 환경 경고 (루트에 semo-core가 있는 경우)
175
178
  const hasLegacyCore = fs.existsSync(path.join(cwd, "semo-core"));
176
- const hasLegacySkills = fs.existsSync(path.join(cwd, "semo-skills"));
177
- if (hasLegacyCore || hasLegacySkills) {
179
+ if (hasLegacyCore) {
178
180
  spinner.warn("레거시 환경 감지됨");
179
181
  console.log(chalk_1.default.yellow("\n ⚠️ 구버전 SEMO 구조가 감지되었습니다."));
180
- console.log(chalk_1.default.gray(" 루트에 semo-core/ 또는 semo-skills/가 있습니다."));
182
+ console.log(chalk_1.default.gray(" 루트에 semo-core/가 있습니다."));
181
183
  console.log(chalk_1.default.cyan("\n 💡 마이그레이션 방법:"));
182
- console.log(chalk_1.default.gray(" 1. 기존 semo-core/, semo-skills/ 폴더 삭제"));
184
+ console.log(chalk_1.default.gray(" 1. 기존 semo-core/ 폴더 삭제"));
183
185
  console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
184
186
  console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
185
187
  console.log(chalk_1.default.gray(" 또는: semo migrate --force\n"));
@@ -198,19 +200,7 @@ async function showVersionComparison(cwd) {
198
200
  level: 0,
199
201
  });
200
202
  }
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
- }
203
+ // semo-skills 제거됨 중앙 DB 단일 SoT
214
204
  // semo-agents (semo-system/ 내부)
215
205
  const agentsPathSystem = path.join(semoSystemDir, "semo-agents", "VERSION");
216
206
  if (fs.existsSync(agentsPathSystem)) {
@@ -384,13 +374,13 @@ function isSymlinkValid(linkPath) {
384
374
  }
385
375
  /**
386
376
  * 레거시 SEMO 환경을 감지합니다.
387
- * 레거시: 프로젝트 루트에 semo-core/, semo-skills/ 가 직접 있는 경우
377
+ * 레거시: 프로젝트 루트에 semo-core/ 가 직접 있는 경우
388
378
  * 신규: semo-system/ 하위에 있는 경우
389
379
  */
390
380
  function detectLegacyEnvironment(cwd) {
391
381
  const legacyPaths = [];
392
382
  // 루트에 직접 있는 레거시 디렉토리 확인
393
- const legacyDirs = ["semo-core", "semo-skills", "sax-core", "sax-skills"];
383
+ const legacyDirs = ["semo-core", "sax-core", "sax-skills"];
394
384
  for (const dir of legacyDirs) {
395
385
  const dirPath = path.join(cwd, dir);
396
386
  if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
@@ -452,7 +442,7 @@ async function migrateLegacyEnvironment(cwd) {
452
442
  if (!shouldMigrate) {
453
443
  console.log(chalk_1.default.yellow("\n마이그레이션이 취소되었습니다."));
454
444
  console.log(chalk_1.default.gray("💡 수동 마이그레이션 방법:"));
455
- console.log(chalk_1.default.gray(" 1. 기존 semo-core/, semo-skills/ 폴더 삭제"));
445
+ console.log(chalk_1.default.gray(" 1. 기존 레거시 폴더 삭제"));
456
446
  console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
457
447
  console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
458
448
  return false;
@@ -460,7 +450,7 @@ async function migrateLegacyEnvironment(cwd) {
460
450
  const spinner = (0, ora_1.default)("레거시 환경 마이그레이션 중...").start();
461
451
  try {
462
452
  // 1. 루트의 레거시 디렉토리 삭제
463
- const legacyDirs = ["semo-core", "semo-skills", "sax-core", "sax-skills"];
453
+ const legacyDirs = ["semo-core", "sax-core", "sax-skills"];
464
454
  for (const dir of legacyDirs) {
465
455
  const dirPath = path.join(cwd, dir);
466
456
  if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
@@ -579,21 +569,6 @@ async function showVersionInfo() {
579
569
  level: 0,
580
570
  });
581
571
  }
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
572
  // 결과 출력
598
573
  const needsUpdateCount = versionInfos.filter(v => v.needsUpdate).length;
599
574
  if (versionInfos.length === 1) {
@@ -676,11 +651,6 @@ async function confirmOverwrite(itemName, itemPath) {
676
651
  if (!fs.existsSync(itemPath)) {
677
652
  return true;
678
653
  }
679
- // 비인터랙티브 환경(CI, 파이프) — 덮어쓰지 않고 기존 파일 유지
680
- if (!process.stdin.isTTY) {
681
- console.log(chalk_1.default.gray(` → ${itemName} 이미 존재 (비인터랙티브 모드: 건너뜀)`));
682
- return false;
683
- }
684
654
  const { shouldOverwrite } = await inquirer_1.default.prompt([
685
655
  {
686
656
  type: "confirm",
@@ -769,36 +739,75 @@ async function showToolsStatus() {
769
739
  }
770
740
  return true;
771
741
  }
772
- // === init 명령어 ===
742
+ // === 글로벌 설정 체크 ===
743
+ function isGlobalSetupDone() {
744
+ const home = os.homedir();
745
+ return (fs.existsSync(path.join(home, ".semo.env")) &&
746
+ fs.existsSync(path.join(home, ".claude", "skills")));
747
+ }
748
+ // === onboarding 명령어 (글로벌 1회 설정) ===
773
749
  program
774
- .command("init")
775
- .description("현재 프로젝트에 SEMO 설치합니다")
750
+ .command("onboarding")
751
+ .description("글로벌 SEMO 설정 (머신당 1회) — ~/.claude/, ~/.semo.env")
752
+ .option("--credentials-gist <gistId>", "Private GitHub Gist에서 DB 접속정보 가져오기")
776
753
  .option("-f, --force", "기존 설정 덮어쓰기")
777
754
  .option("--skip-mcp", "MCP 설정 생략")
755
+ .action(async (options) => {
756
+ console.log(chalk_1.default.cyan.bold("\n🏠 SEMO 글로벌 온보딩\n"));
757
+ console.log(chalk_1.default.gray(" 대상: ~/.claude/, ~/.semo.env (머신당 1회)\n"));
758
+ // 1. ~/.semo.env DB 접속 설정
759
+ await setupSemoEnv(options.credentialsGist, options.force);
760
+ // 2. DB health check
761
+ const spinner = (0, ora_1.default)("DB 연결 확인 중...").start();
762
+ const connected = await (0, database_1.isDbConnected)();
763
+ if (connected) {
764
+ spinner.succeed("DB 연결 확인됨");
765
+ }
766
+ else {
767
+ spinner.warn("DB 연결 실패 — 스킬/커맨드/에이전트 설치를 건너뜁니다");
768
+ console.log(chalk_1.default.gray(" ~/.semo.env를 확인하고 다시 시도하세요: semo onboarding\n"));
769
+ await (0, database_1.closeConnection)();
770
+ return;
771
+ }
772
+ // 3. Standard 설치 (DB → ~/.claude/skills, commands, agents)
773
+ await setupStandardGlobal();
774
+ // 4. Hooks 설치 (프로젝트 무관)
775
+ await setupHooks(false);
776
+ // 5. MCP 설정 (글로벌)
777
+ if (!options.skipMcp) {
778
+ await setupMCP(os.homedir(), [], options.force || false);
779
+ }
780
+ await (0, database_1.closeConnection)();
781
+ // 결과 요약
782
+ console.log(chalk_1.default.green.bold("\n✅ SEMO 글로벌 온보딩 완료!\n"));
783
+ console.log(chalk_1.default.cyan("설치된 구성:"));
784
+ console.log(chalk_1.default.gray(" ~/.semo.env DB 접속정보 (권한 600)"));
785
+ console.log(chalk_1.default.gray(" ~/.claude/skills/ 팀 스킬 (DB 기반)"));
786
+ console.log(chalk_1.default.gray(" ~/.claude/commands/ 팀 커맨드 (DB 기반)"));
787
+ console.log(chalk_1.default.gray(" ~/.claude/agents/ 팀 에이전트 (DB 기반, dedup)"));
788
+ console.log(chalk_1.default.gray(" ~/.claude/settings.local.json SessionStart/Stop 훅"));
789
+ console.log(chalk_1.default.cyan("\n다음 단계:"));
790
+ console.log(chalk_1.default.gray(" 프로젝트 디렉토리에서 'semo init'을 실행하세요."));
791
+ console.log();
792
+ });
793
+ // === init 명령어 (프로젝트별 설정) ===
794
+ program
795
+ .command("init")
796
+ .description("현재 프로젝트에 SEMO 프로젝트 설정을 합니다 (글로벌: semo onboarding)")
797
+ .option("-f, --force", "기존 설정 덮어쓰기")
778
798
  .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
799
  .action(async (options) => {
783
- console.log(chalk_1.default.cyan.bold("\n🚀 SEMO 설치 시작\n"));
800
+ console.log(chalk_1.default.cyan.bold("\n📁 SEMO 프로젝트 설정\n"));
784
801
  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);
802
+ // 0. 글로벌 설정 확인
803
+ if (!isGlobalSetupDone()) {
804
+ console.log(chalk_1.default.yellow("⚠ 글로벌 설정이 완료되지 않았습니다."));
805
+ console.log(chalk_1.default.gray(" 먼저 'semo onboarding'을 실행하세요.\n"));
806
+ console.log(chalk_1.default.gray(" 이 머신에서 처음 SEMO를 사용하시나요?"));
807
+ console.log(chalk_1.default.cyan(" → semo onboarding --credentials-gist <GIST_ID>\n"));
808
+ process.exit(1);
800
809
  }
801
- // 1.5. Git 레포지토리 확인
810
+ // 1. Git 레포지토리 확인
802
811
  const spinner = (0, ora_1.default)("Git 레포지토리 확인 중...").start();
803
812
  try {
804
813
  (0, child_process_1.execSync)("git rev-parse --git-dir", { cwd, stdio: "pipe" });
@@ -814,64 +823,48 @@ program
814
823
  fs.mkdirSync(claudeDir, { recursive: true });
815
824
  console.log(chalk_1.default.green("\n✓ .claude/ 디렉토리 생성됨"));
816
825
  }
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);
826
+ // 3. 로컬 스킬 경고 (기존 프로젝트 호환)
827
+ const localSkillsDir = path.join(claudeDir, "skills");
828
+ if (fs.existsSync(localSkillsDir)) {
829
+ try {
830
+ const localSkills = fs.readdirSync(localSkillsDir).filter(f => fs.statSync(path.join(localSkillsDir, f)).isDirectory());
831
+ if (localSkills.length > 0) {
832
+ console.log(chalk_1.default.yellow(`\nℹ 프로젝트 로컬 스킬이 감지되었습니다 (${localSkills.length}개).`));
833
+ console.log(chalk_1.default.gray(" 글로벌 스킬(~/.claude/skills/)이 우선 적용됩니다."));
834
+ console.log(chalk_1.default.gray(" 로컬 스킬을 제거하려면: rm -rf .claude/skills/ .claude/commands/ .claude/agents/\n"));
835
+ }
836
+ }
837
+ catch {
838
+ // ignore
839
+ }
824
840
  }
825
- // 5. Context Mesh 초기화
841
+ // 4. Context Mesh 초기화
826
842
  await setupContextMesh(cwd);
843
+ // 5. CLAUDE.md 생성 (프로젝트 규칙만, 스킬 목록 없음)
844
+ await setupClaudeMd(cwd, [], options.force || false);
827
845
  // 6. .gitignore 업데이트
828
846
  if (options.gitignore !== false) {
829
847
  updateGitignore(cwd);
830
848
  }
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
849
  // 완료 메시지
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 (훅 등록)"));
850
+ console.log(chalk_1.default.green.bold("\n✅ SEMO 프로젝트 설정 완료!\n"));
851
+ console.log(chalk_1.default.cyan("생성된 파일:"));
852
+ console.log(chalk_1.default.gray(" {cwd}/.claude/CLAUDE.md 프로젝트 규칙"));
853
+ console.log(chalk_1.default.gray(" {cwd}/.claude/memory/context.md 프로젝트 상태"));
854
+ console.log(chalk_1.default.gray(" {cwd}/.claude/memory/decisions.md ADR"));
855
+ console.log(chalk_1.default.gray(" {cwd}/.claude/memory/projects.md 프로젝트 맵"));
857
856
  console.log(chalk_1.default.cyan("\n다음 단계:"));
858
857
  console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기 (SessionStart 훅이 자동 sync)"));
859
858
  console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
860
859
  console.log(chalk_1.default.gray(" 3. /SEMO:help로 도움말 확인"));
861
860
  console.log();
862
- console.log(chalk_1.default.gray(" DB 접속정보 변경: nano ~/.semo.env"));
863
- console.log();
864
861
  });
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"));
862
+ // === Standard 설치 (DB 기반, 글로벌 ~/.claude/) ===
863
+ async function setupStandardGlobal() {
864
+ console.log(chalk_1.default.cyan("\n📚 Standard 설치 (DB → ~/.claude/)"));
865
+ console.log(chalk_1.default.gray(" 스킬/커맨드/에이전트를 글로벌에 설치\n"));
872
866
  const spinner = (0, ora_1.default)("DB에서 스킬/커맨드/에이전트 조회 중...").start();
873
867
  try {
874
- // DB 연결 확인
875
868
  const connected = await (0, database_1.isDbConnected)();
876
869
  if (connected) {
877
870
  spinner.text = "DB 연결 성공, 데이터 조회 중...";
@@ -879,174 +872,18 @@ async function setupStandard(cwd, force) {
879
872
  else {
880
873
  spinner.text = "DB 연결 실패, 폴백 데이터 사용 중...";
881
874
  }
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 기반)");
875
+ const result = await (0, global_cache_1.syncGlobalCache)();
876
+ console.log(chalk_1.default.green(` ✓ skills 설치 완료 (${result.skills})`));
877
+ console.log(chalk_1.default.green(` ✓ commands 설치 완료 (${result.commands}개)`));
878
+ console.log(chalk_1.default.green(` ✓ agents 설치 완료 (${result.agents}개)`));
879
+ spinner.succeed("Standard 설치 완료 (DB ~/.claude/)");
936
880
  }
937
881
  catch (error) {
938
882
  spinner.fail("Standard 설치 실패");
939
883
  console.error(chalk_1.default.red(` ${error}`));
940
884
  }
941
885
  }
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
- }
886
+ // (generateClaudeMd removed — setupClaudeMd handles project CLAUDE.md generation)
1050
887
  // === Standard 심볼릭 링크 (레거시 호환) ===
1051
888
  async function createStandardSymlinks(cwd) {
1052
889
  const claudeDir = path.join(cwd, ".claude");
@@ -1070,29 +907,9 @@ async function createStandardSymlinks(cwd) {
1070
907
  }
1071
908
  console.log(chalk_1.default.green(` ✓ .claude/agents/ (${agents.length}개 agent 링크됨)`));
1072
909
  }
1073
- // skills 디렉토리 생성 개별 링크 (DB 기반 - 활성 스킬만)
910
+ // skills 중앙 DB 단일 SoT (semo-skills 파일시스템 제거됨)
1074
911
  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
- }
912
+ fs.mkdirSync(claudeSkillsDir, { recursive: true });
1096
913
  // commands 링크
1097
914
  const commandsDir = path.join(claudeDir, "commands");
1098
915
  fs.mkdirSync(commandsDir, { recursive: true });
@@ -1183,15 +1000,10 @@ function verifyInstallation(cwd, installedExtensions = []) {
1183
1000
  }
1184
1001
  // === 레거시: semo-system 기반 설치 검증 ===
1185
1002
  const coreDir = path.join(semoSystemDir, "semo-core");
1186
- const skillsDir = path.join(semoSystemDir, "semo-skills");
1187
1003
  if (!fs.existsSync(coreDir)) {
1188
1004
  result.errors.push("semo-core가 설치되지 않았습니다");
1189
1005
  result.success = false;
1190
1006
  }
1191
- if (!fs.existsSync(skillsDir)) {
1192
- result.errors.push("semo-skills가 설치되지 않았습니다");
1193
- result.success = false;
1194
- }
1195
1007
  // 2. agents 링크 검증 (isSymlinkValid 사용)
1196
1008
  const claudeAgentsDir = path.join(claudeDir, "agents");
1197
1009
  const coreAgentsDir = path.join(coreDir, "agents");
@@ -1218,31 +1030,7 @@ function verifyInstallation(cwd, installedExtensions = []) {
1218
1030
  }
1219
1031
  }
1220
1032
  }
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
- }
1033
+ // 3. skills 중앙 DB 단일 SoT (파일시스템 검증 불필요)
1246
1034
  // 4. commands 검증 (isSymlinkValid 사용)
1247
1035
  const semoCommandsLink = path.join(claudeDir, "commands", "SEMO");
1248
1036
  try {
@@ -1372,110 +1160,156 @@ const BASE_MCP_SERVERS = [
1372
1160
  name: "context7",
1373
1161
  command: "npx",
1374
1162
  args: ["-y", "@upstash/context7-mcp"],
1163
+ scope: "user",
1375
1164
  },
1376
1165
  {
1377
1166
  name: "sequential-thinking",
1378
1167
  command: "npx",
1379
1168
  args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
1169
+ scope: "user",
1380
1170
  },
1381
1171
  {
1382
1172
  name: "playwright",
1383
1173
  command: "npx",
1384
1174
  args: ["-y", "@anthropic-ai/mcp-server-playwright"],
1175
+ scope: "user",
1385
1176
  },
1386
1177
  {
1387
1178
  name: "github",
1388
1179
  command: "npx",
1389
1180
  args: ["-y", "@modelcontextprotocol/server-github"],
1181
+ scope: "user",
1182
+ },
1183
+ ];
1184
+ const SEMO_CREDENTIALS = [
1185
+ {
1186
+ key: "DATABASE_URL",
1187
+ required: true,
1188
+ sensitive: true,
1189
+ description: "팀 코어 PostgreSQL 연결 URL",
1190
+ promptMessage: "DATABASE_URL:",
1191
+ },
1192
+ {
1193
+ key: "OPENAI_API_KEY",
1194
+ required: false,
1195
+ sensitive: true,
1196
+ description: "OpenAI API 키 (KB 임베딩용)",
1197
+ promptMessage: "OPENAI_API_KEY (없으면 Enter):",
1198
+ },
1199
+ {
1200
+ key: "SLACK_WEBHOOK",
1201
+ required: false,
1202
+ sensitive: false,
1203
+ description: "Slack 알림 Webhook (선택)",
1204
+ promptMessage: "SLACK_WEBHOOK (없으면 Enter):",
1390
1205
  },
1391
1206
  ];
1392
- // === ~/.semo.env 설정 (자동 감지 → Gist → 프롬프트) ===
1393
- function writeSemoEnvFile(dbUrl, slackWebhook = "") {
1207
+ function writeSemoEnvFile(creds) {
1394
1208
  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 });
1209
+ const lines = [
1210
+ "# SEMO 환경변수 모든 컨텍스트에서 자동 로드됨",
1211
+ "# (Claude Code 앱, OpenClaw LaunchAgent, cron 등)",
1212
+ "",
1213
+ ];
1214
+ // 레지스트리 먼저 (순서 보장)
1215
+ for (const def of SEMO_CREDENTIALS) {
1216
+ const val = creds[def.key] || "";
1217
+ lines.push(`# ${def.description}`);
1218
+ lines.push(`${def.key}='${val}'`);
1219
+ lines.push("");
1220
+ }
1221
+ // 레지스트리 외 추가 키
1222
+ for (const [k, v] of Object.entries(creds)) {
1223
+ if (!SEMO_CREDENTIALS.some((d) => d.key === k)) {
1224
+ lines.push(`${k}='${v}'`);
1225
+ }
1226
+ }
1227
+ lines.push("");
1228
+ fs.writeFileSync(envFile, lines.join("\n"), { mode: 0o600 });
1404
1229
  }
1405
- function readSemoEnvDbUrl() {
1230
+ function readSemoEnvCreds() {
1406
1231
  const envFile = path.join(os.homedir(), ".semo.env");
1407
1232
  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;
1233
+ return {};
1234
+ try {
1235
+ return (0, env_parser_1.parseEnvContent)(fs.readFileSync(envFile, "utf-8"));
1236
+ }
1237
+ catch {
1238
+ return {};
1239
+ }
1412
1240
  }
1413
- function fetchDbUrlFromGist(gistId) {
1241
+ function fetchCredsFromGist(gistId) {
1414
1242
  try {
1415
1243
  const raw = (0, child_process_1.execSync)(`gh gist view ${gistId} --raw`, {
1416
1244
  encoding: "utf-8",
1417
1245
  stdio: ["pipe", "pipe", "pipe"],
1418
1246
  timeout: 8000,
1419
1247
  });
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;
1248
+ const creds = (0, env_parser_1.parseEnvContent)(raw);
1249
+ return Object.keys(creds).length > 0 ? creds : null;
1423
1250
  }
1424
1251
  catch {
1425
1252
  return null;
1426
1253
  }
1427
1254
  }
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
1255
+ async function setupSemoEnv(credentialsGist, force) {
1256
+ console.log(chalk_1.default.cyan("\n🔑 환경변수 설정"));
1257
+ // 1. 기존 파일
1258
+ const existing = force ? {} : readSemoEnvCreds();
1259
+ // 2. Gist
1447
1260
  const gistId = credentialsGist || process.env.SEMO_CREDENTIALS_GIST;
1261
+ let gistCreds = {};
1448
1262
  if (gistId) {
1449
1263
  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;
1264
+ gistCreds = fetchCredsFromGist(gistId) || {};
1265
+ }
1266
+ // 3. 머지: gist(base) ← file(override) ← env(override)
1267
+ const merged = { ...gistCreds };
1268
+ for (const [k, v] of Object.entries(existing)) {
1269
+ if (v)
1270
+ merged[k] = v;
1271
+ }
1272
+ for (const def of SEMO_CREDENTIALS) {
1273
+ if (process.env[def.key])
1274
+ merged[def.key] = process.env[def.key];
1275
+ }
1276
+ // 4. required인데 없는 키만 프롬프트
1277
+ let hasNewKeys = false;
1278
+ for (const def of SEMO_CREDENTIALS) {
1279
+ if (merged[def.key]) {
1280
+ const label = def.sensitive ? "(설정됨)" : merged[def.key];
1281
+ console.log(chalk_1.default.green(` ✅ ${def.key} ${label}`));
1282
+ }
1283
+ else if (def.required) {
1284
+ const { value } = await inquirer_1.default.prompt([
1285
+ {
1286
+ type: "password",
1287
+ name: "value",
1288
+ message: def.promptMessage,
1289
+ mask: "*",
1290
+ },
1291
+ ]);
1292
+ if (value?.trim()) {
1293
+ merged[def.key] = value.trim();
1294
+ hasNewKeys = true;
1295
+ }
1296
+ }
1297
+ else {
1298
+ console.log(chalk_1.default.gray(` ⏭ ${def.key} (없음 — 선택사항)`));
1455
1299
  }
1456
- console.log(chalk_1.default.yellow(" ⚠️ Gist fetch 실패 — 수동 입력으로 전환"));
1457
1300
  }
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());
1301
+ // 5. 변경사항이 있거나 파일이 없으면 쓰기
1302
+ const envFile = path.join(os.homedir(), ".semo.env");
1303
+ const needsWrite = force ||
1304
+ hasNewKeys ||
1305
+ !fs.existsSync(envFile) ||
1306
+ Object.keys(gistCreds).some((k) => !existing[k]);
1307
+ if (needsWrite) {
1308
+ writeSemoEnvFile(merged);
1470
1309
  console.log(chalk_1.default.green(" ✅ ~/.semo.env 저장됨 (권한: 600)"));
1471
- console.log(chalk_1.default.gray(` 파일: ${envFile}`));
1472
1310
  }
1473
1311
  else {
1474
- // 템플릿 생성
1475
- if (!fs.existsSync(envFile)) {
1476
- writeSemoEnvFile("");
1477
- }
1478
- console.log(chalk_1.default.yellow(" ⚠️ 건너뜀 — ~/.semo.env에서 DATABASE_URL을 직접 입력하세요"));
1312
+ console.log(chalk_1.default.gray(" ~/.semo.env 변경 없음"));
1479
1313
  }
1480
1314
  }
1481
1315
  // === Claude MCP 서버 존재 여부 확인 ===
@@ -1504,6 +1338,9 @@ function registerMCPServer(server) {
1504
1338
  args.push("-e", `${key}=${value}`);
1505
1339
  }
1506
1340
  }
1341
+ // scope 지정 (기본: project)
1342
+ const scope = server.scope || "project";
1343
+ args.push("-s", scope);
1507
1344
  // -- 구분자 후 명령어와 인자 추가
1508
1345
  args.push("--", server.command, ...server.args);
1509
1346
  (0, child_process_1.execSync)(`claude ${args.join(" ")}`, { stdio: "pipe" });
@@ -1529,23 +1366,17 @@ async function setupMCP(cwd, _extensions, force) {
1529
1366
  const settings = {
1530
1367
  mcpServers: {},
1531
1368
  };
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
- }
1369
+ // settings.json에는 프로젝트 전용 semo-kb만 기록
1370
+ // 공통 서버(context7 등)는 유저레벨에 등록하므로 프로젝트 settings에 쓰지 않음
1371
+ settings.mcpServers["semo-kb"] = {
1372
+ command: "node",
1373
+ args: ["packages/mcp-kb/dist/index.js"],
1374
+ };
1545
1375
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1546
- console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨 (MCP 설정 백업)"));
1547
- // Claude Code에 MCP 서버 등록 시도
1376
+ console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨 (semo-kb MCP 설정)"));
1377
+ // Claude Code에 MCP 서버 등록 시도 (공통 서버는 유저레벨로)
1548
1378
  console.log(chalk_1.default.cyan("\n🔌 Claude Code에 MCP 서버 등록 중..."));
1379
+ const allServers = [...BASE_MCP_SERVERS];
1549
1380
  const successServers = [];
1550
1381
  const skippedServers = [];
1551
1382
  const failedServers = [];
@@ -1628,12 +1459,13 @@ semo-system/
1628
1459
  }
1629
1460
  }
1630
1461
  // === Hooks 설치/업데이트 ===
1631
- async function setupHooks(cwd, isUpdate = false) {
1462
+ async function setupHooks(isUpdate = false) {
1632
1463
  const action = isUpdate ? "업데이트" : "설치";
1633
1464
  console.log(chalk_1.default.cyan(`\n🪝 Claude Code Hooks ${action}`));
1634
- const homeDir = process.env.HOME || process.env.USERPROFILE || "";
1465
+ console.log(chalk_1.default.gray(" semo CLI 기반 컨텍스트 동기화\n"));
1466
+ const homeDir = os.homedir();
1635
1467
  const settingsPath = path.join(homeDir, ".claude", "settings.local.json");
1636
- // Core 훅: semo context sync/push — semo-hooks 유무와 무관하게 항상 등록
1468
+ // hooks 설정 객체 — semo CLI만 사용 (프로젝트 경로 무관)
1637
1469
  const hooksConfig = {
1638
1470
  SessionStart: [
1639
1471
  {
@@ -1660,36 +1492,6 @@ async function setupHooks(cwd, isUpdate = false) {
1660
1492
  },
1661
1493
  ],
1662
1494
  };
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
1495
  // 기존 설정 로드 또는 새로 생성
1694
1496
  let existingSettings = {};
1695
1497
  const claudeConfigDir = path.join(homeDir, ".claude");
@@ -1890,132 +1692,86 @@ async function setupClaudeMd(cwd, _extensions, force) {
1890
1692
  return;
1891
1693
  }
1892
1694
  }
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와 자동 동기화됩니다.
1695
+ // 프로젝트 규칙만 (스킬/에이전트 목록은 글로벌 ~/.claude/에 있음)
1696
+ const claudeMdContent = `# SEMO Project Configuration
1923
1697
 
1924
- | 시점 | 동작 |
1925
- |------|------|
1926
- | 세션 시작 | \`semo context sync\` → \`.claude/memory/\` 최신화 |
1927
- | 세션 종료 | \`semo context push\` → \`decisions.md\` 변경분 DB 저장 |
1698
+ > SEMO (Semicolon Orchestrate) - AI Agent Orchestration Framework v${VERSION}
1699
+ > 스킬/에이전트/커맨드는 글로벌(~/.claude/)에서 로드됩니다.
1928
1700
 
1929
1701
  ---
1930
1702
 
1931
- ## Memory Context
1932
-
1933
- \`.claude/memory/\` 파일들은 **팀 Core DB (\`semo\` 스키마)에서 자동으로 채워집니다**.
1934
- 직접 편집하지 마세요 — 세션 시작 시 덮어씌워집니다.
1703
+ ## KB 접근 (semo-kb MCP 서버)
1935
1704
 
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 → 로컬 (읽기 전용) |
1705
+ KB 데이터는 **semo-kb MCP 서버**를 통해 Core DB에서 실시간 조회합니다.
1945
1706
 
1946
- **decisions.md 편집 가능합니다.** 아키텍처 결정(ADR)을 여기에 기록하세요.
1707
+ | MCP 도구 | 설명 |
1708
+ |----------|------|
1709
+ | \`kb_search\` | 벡터+텍스트 하이브리드 검색 (query, domain?, limit?, mode?) |
1710
+ | \`kb_get\` | domain+key 정확 조회 |
1711
+ | \`kb_list\` | 도메인별 엔트리 목록 |
1712
+ | \`kb_upsert\` | KB 항목 쓰기 (OpenAI 임베딩 자동 생성) |
1713
+ | \`kb_bot_status\` | 봇 상태 테이블 조회 |
1714
+ | \`kb_ontology\` | 온톨로지 스키마 조회 |
1715
+ | \`kb_digest\` | 봇 구독 도메인 변경 다이제스트 |
1947
1716
 
1948
1717
  ---
1949
1718
 
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\`에 있습니다.
1719
+ ## KB-First 행동 규칙 (NON-NEGOTIABLE)
1968
1720
 
1969
- ### 기술 스택
1721
+ > KB는 팀의 Single Source of Truth이다. 아래 규칙은 예외 없이 적용된다.
1970
1722
 
1971
- <!-- 예: Next.js 14, PostgreSQL, TypeScript strict mode -->
1723
+ ### 읽기 (Query-First)
1724
+ 다음 주제 질문 → **반드시 kb_search/kb_get으로 KB 먼저 조회** 후 답변:
1725
+ - 팀원 정보 → \`domain: team\`
1726
+ - 프로젝트 현황 → \`domain: project\`
1727
+ - 의사결정 기록 → \`domain: decision\`
1728
+ - 업무 프로세스 → \`domain: process\`
1729
+ - 인프라 구성 → \`domain: infra\`
1730
+ - KPI → \`domain: kpi\`
1972
1731
 
1973
- ### 브랜치 전략
1732
+ **금지:** 주제를 자체 지식/세션 기억만으로 답변하는 것.
1733
+ KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
1974
1734
 
1975
- <!-- 예: main(prod) / dev(staging) / feat/* -->
1735
+ ### 쓰기 (Write-Back)
1736
+ 다음 상황 → **반드시 kb_upsert로 KB에 즉시 기록:**
1737
+ - 사용자가 팀 정보를 정정하거나 새 사실을 알려줄 때
1738
+ - 의사결정이 내려졌을 때
1739
+ - 프로세스/규칙이 변경되었을 때
1976
1740
 
1977
- ### 코딩 컨벤션
1978
-
1979
- <!-- 예: ESLint airbnb, 함수형 컴포넌트 필수, any 금지 -->
1980
-
1981
- ### 아키텍처 특이사항
1982
-
1983
- <!-- 예: DB 직접 접근 금지 — 반드시 API route 통해야 함 -->
1741
+ **금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
1984
1742
 
1985
1743
  ---
1986
1744
 
1987
- ## Quality Gate
1745
+ ## Pre-Commit Quality Gate
1988
1746
 
1989
- 코드 변경 커밋 전 필수:
1747
+ > **코드 변경이 포함된 커밋 전 반드시 Quality Gate를 통과해야 합니다.**
1990
1748
 
1991
1749
  \`\`\`bash
1992
- npm run lint # ESLint
1993
- npx tsc --noEmit # TypeScript
1994
- npm run build # 빌드 검증
1750
+ npm run lint # ESLint 검사
1751
+ npx tsc --noEmit # TypeScript 타입 체크
1752
+ npm run build # 빌드 검증
1995
1753
  \`\`\`
1996
1754
 
1997
- \`--no-verify\` 사용 금지.
1755
+ \`--no-verify\` 플래그 사용 금지.
1998
1756
 
1999
1757
  ---
2000
1758
 
2001
- ## 슬래시 커맨드
1759
+ ## 환경변수 (\`~/.semo.env\`)
2002
1760
 
2003
- | 커맨드 | 설명 |
2004
- |--------|------|
2005
- | \`/SEMO:help\` | 도움말 |
2006
- | \`/SEMO:feedback\` | 피드백 제출 |
2007
- | \`/SEMO:health\` | 환경 헬스체크 |
1761
+ SEMO는 \`~/.semo.env\` 파일에서 공통 환경변수를 로드합니다.
1762
+ SessionStart 훅과 OpenClaw 게이트웨이 래퍼에서 자동 source됩니다.
2008
1763
 
2009
- ---
1764
+ | 변수 | 용도 | 필수 |
1765
+ |------|------|------|
1766
+ | \`DATABASE_URL\` | 팀 Core DB (PostgreSQL) 연결 | ✅ |
1767
+ | \`OPENAI_API_KEY\` | KB 임베딩용 (text-embedding-3-small) | 선택 |
1768
+ | \`SLACK_WEBHOOK\` | Slack 알림 | 선택 |
2010
1769
 
2011
- ## 복구 명령어
1770
+ 갱신이 필요하면 \`~/.semo.env\`를 직접 편집하거나 \`semo onboarding -f\`를 실행하세요.
2012
1771
 
2013
- \`\`\`bash
2014
- semo doctor # 환경 진단 (DB 연결, 설치 상태)
2015
- semo config db # DB URL 재설정
2016
- semo context sync # memory/ 수동 최신화
2017
- semo bots status # 봇 상태 조회
2018
- \`\`\`
1772
+ ---
1773
+
1774
+ > Generated by SEMO CLI v${VERSION}
2019
1775
  `;
2020
1776
  fs.writeFileSync(claudeMdPath, claudeMdContent);
2021
1777
  console.log(chalk_1.default.green("✓ .claude/CLAUDE.md 생성됨"));
@@ -2030,7 +1786,7 @@ program
2030
1786
  console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록\n"));
2031
1787
  // Standard 패키지 표시
2032
1788
  console.log(chalk_1.default.white.bold("Standard (필수)"));
2033
- const standardPkgs = ["semo-core", "semo-skills", "semo-agents", "semo-scripts"];
1789
+ const standardPkgs = ["semo-core", "semo-agents", "semo-scripts"];
2034
1790
  for (const pkg of standardPkgs) {
2035
1791
  const isInstalled = fs.existsSync(path.join(semoSystemDir, pkg));
2036
1792
  console.log(` ${isInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} ${pkg}`);
@@ -2063,7 +1819,6 @@ program
2063
1819
  console.log(chalk_1.default.white.bold("Standard:"));
2064
1820
  const standardChecks = [
2065
1821
  { name: "semo-core", path: path.join(semoSystemDir, "semo-core") },
2066
- { name: "semo-skills", path: path.join(semoSystemDir, "semo-skills") },
2067
1822
  ];
2068
1823
  let standardOk = true;
2069
1824
  for (const check of standardChecks) {
@@ -2101,11 +1856,27 @@ program
2101
1856
  .command("update")
2102
1857
  .description("SEMO를 최신 버전으로 업데이트합니다")
2103
1858
  .option("--self", "CLI만 업데이트")
1859
+ .option("--global", "글로벌 스킬/커맨드/에이전트를 DB 최신으로 갱신 (~/.claude/)")
2104
1860
  .option("--system", "semo-system만 업데이트")
2105
1861
  .option("--skip-cli", "CLI 업데이트 건너뛰기")
2106
- .option("--only <packages>", "특정 패키지만 업데이트 (쉼표 구분: semo-core,semo-skills,biz/management)")
1862
+ .option("--only <packages>", "특정 패키지만 업데이트 (쉼표 구분: semo-core,biz/management)")
2107
1863
  .option("--migrate", "레거시 환경 강제 마이그레이션")
2108
1864
  .action(async (options) => {
1865
+ // === --global: 글로벌 스킬 갱신 ===
1866
+ if (options.global) {
1867
+ console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 글로벌 업데이트\n"));
1868
+ console.log(chalk_1.default.gray(" 대상: ~/.claude/skills, commands, agents (DB 최신)\n"));
1869
+ const connected = await (0, database_1.isDbConnected)();
1870
+ if (!connected) {
1871
+ console.log(chalk_1.default.red(" DB 연결 실패 — ~/.semo.env를 확인하세요."));
1872
+ await (0, database_1.closeConnection)();
1873
+ process.exit(1);
1874
+ }
1875
+ await setupStandardGlobal();
1876
+ await (0, database_1.closeConnection)();
1877
+ console.log(chalk_1.default.green.bold("\n✅ 글로벌 스킬 업데이트 완료!\n"));
1878
+ return;
1879
+ }
2109
1880
  console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 업데이트\n"));
2110
1881
  const cwd = process.cwd();
2111
1882
  const semoSystemDir = path.join(cwd, "semo-system");
@@ -2159,22 +1930,19 @@ program
2159
1930
  }
2160
1931
  // 업데이트 대상 결정
2161
1932
  const updateSemoCore = !isSelectiveUpdate || onlyPackages.includes("semo-core");
2162
- const updateSemoSkills = !isSelectiveUpdate || onlyPackages.includes("semo-skills");
2163
1933
  const updateSemoAgents = !isSelectiveUpdate || onlyPackages.includes("semo-agents");
2164
1934
  const updateSemoScripts = !isSelectiveUpdate || onlyPackages.includes("semo-scripts");
2165
1935
  console.log(chalk_1.default.cyan("\n📚 semo-system 업데이트"));
2166
1936
  console.log(chalk_1.default.gray(" 대상:"));
2167
1937
  if (updateSemoCore)
2168
1938
  console.log(chalk_1.default.gray(" - semo-core"));
2169
- if (updateSemoSkills)
2170
- console.log(chalk_1.default.gray(" - semo-skills"));
2171
1939
  if (updateSemoAgents)
2172
1940
  console.log(chalk_1.default.gray(" - semo-agents"));
2173
1941
  if (updateSemoScripts)
2174
1942
  console.log(chalk_1.default.gray(" - semo-scripts"));
2175
- if (!updateSemoCore && !updateSemoSkills && !updateSemoAgents && !updateSemoScripts) {
1943
+ if (!updateSemoCore && !updateSemoAgents && !updateSemoScripts) {
2176
1944
  console.log(chalk_1.default.yellow("\n ⚠️ 업데이트할 패키지가 없습니다."));
2177
- console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-skills, semo-agents, semo-scripts"));
1945
+ console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-agents, semo-scripts"));
2178
1946
  return;
2179
1947
  }
2180
1948
  const spinner = (0, ora_1.default)("\n 최신 버전 다운로드 중...").start();
@@ -2185,7 +1953,6 @@ program
2185
1953
  // Standard 업데이트 (선택적) - semo-system/ 하위에서 복사
2186
1954
  const standardUpdates = [
2187
1955
  { flag: updateSemoCore, name: "semo-core" },
2188
- { flag: updateSemoSkills, name: "semo-skills" },
2189
1956
  { flag: updateSemoAgents, name: "semo-agents" },
2190
1957
  { flag: updateSemoScripts, name: "semo-scripts" },
2191
1958
  ];
@@ -2272,7 +2039,7 @@ program
2272
2039
  }
2273
2040
  }
2274
2041
  // === 6. Hooks 업데이트 ===
2275
- await setupHooks(cwd, true);
2042
+ await setupHooks(true);
2276
2043
  // === 7. 설치 검증 ===
2277
2044
  const verificationResult = verifyInstallation(cwd, []);
2278
2045
  printVerificationResult(verificationResult);
@@ -2331,20 +2098,22 @@ program
2331
2098
  // === config 명령어 (설치 후 설정 변경) ===
2332
2099
  const configCmd = program.command("config").description("SEMO 설정 관리");
2333
2100
  configCmd
2334
- .command("db")
2335
- .description(" Core DB 접속정보 설정 (DATABASE_URL ~/.semo.env)")
2101
+ .command("env")
2102
+ .description("SEMO 환경변수 설정 (DATABASE_URL, OPENAI_API_KEY )")
2336
2103
  .option("--credentials-gist <gistId>", "Private GitHub Gist에서 자동 가져오기")
2104
+ .option("--force", "기존 값도 Gist에서 덮어쓰기")
2337
2105
  .action(async (options) => {
2338
- console.log(chalk_1.default.cyan.bold("\n🔑 DB 접속정보 설정\n"));
2339
- // 기존 확인
2340
- const existing = readSemoEnvDbUrl();
2341
- if (existing) {
2106
+ console.log(chalk_1.default.cyan.bold("\n🔑 SEMO 환경변수 설정\n"));
2107
+ const existing = readSemoEnvCreds();
2108
+ const hasExisting = Object.keys(existing).length > 0;
2109
+ if (hasExisting && !options.force) {
2110
+ const keys = Object.keys(existing).join(", ");
2342
2111
  const { overwrite } = await inquirer_1.default.prompt([
2343
2112
  {
2344
2113
  type: "confirm",
2345
2114
  name: "overwrite",
2346
- message: `기존 DATABASE_URL이 있습니다. 덮어쓰시겠습니까?`,
2347
- default: false,
2115
+ message: `기존 설정이 있습니다 (${keys}). Gist에서 없는 키를 보충하시겠습니까?`,
2116
+ default: true,
2348
2117
  },
2349
2118
  ]);
2350
2119
  if (!overwrite) {
@@ -2352,34 +2121,8 @@ configCmd
2352
2121
  return;
2353
2122
  }
2354
2123
  }
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
- }
2124
+ await setupSemoEnv(options.credentialsGist, options.force);
2125
+ console.log(chalk_1.default.gray(" 다음 Claude Code 세션부터 자동으로 적용됩니다."));
2383
2126
  });
2384
2127
  // === doctor 명령어 (설치 상태 진단) ===
2385
2128
  program
@@ -2410,7 +2153,7 @@ program
2410
2153
  console.log(chalk_1.default.gray(" 💡 해결: semo init 실행"));
2411
2154
  }
2412
2155
  else {
2413
- const packages = ["semo-core", "semo-skills", "semo-agents", "semo-scripts"];
2156
+ const packages = ["semo-core", "semo-agents", "semo-scripts"];
2414
2157
  for (const pkg of packages) {
2415
2158
  const pkgPath = path.join(semoSystemDir, pkg);
2416
2159
  if (fs.existsSync(pkgPath)) {
@@ -2484,19 +2227,7 @@ function readSyncState(cwd) {
2484
2227
  }
2485
2228
  catch { /* */ }
2486
2229
  }
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";
2230
+ return { lastPull: null, lastPush: null, sharedCount: 0 };
2500
2231
  }
2501
2232
  const kbCmd = program
2502
2233
  .command("kb")
@@ -2504,16 +2235,14 @@ const kbCmd = program
2504
2235
  kbCmd
2505
2236
  .command("pull")
2506
2237
  .description("DB에서 KB를 로컬 .kb/로 내려받기")
2507
- .option("--bot <name>", "봇 ID", detectBotId())
2508
2238
  .option("--domain <name>", "특정 도메인만")
2509
2239
  .action(async (options) => {
2510
2240
  const spinner = (0, ora_1.default)("KB 데이터 가져오는 중...").start();
2511
2241
  try {
2512
2242
  const pool = (0, database_1.getPool)();
2513
- const result = await (0, kb_1.kbPull)(pool, options.bot, options.domain, process.cwd());
2243
+ const result = await (0, kb_1.kbPull)(pool, options.domain, process.cwd());
2514
2244
  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}건`));
2245
+ console.log(chalk_1.default.green(` 📦 KB: ${result.length}건`));
2517
2246
  // Also pull ontology
2518
2247
  const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
2519
2248
  console.log(chalk_1.default.green(` 📐 온톨로지: ${ontoCount}개 도메인`));
@@ -2529,9 +2258,8 @@ kbCmd
2529
2258
  kbCmd
2530
2259
  .command("push")
2531
2260
  .description("로컬 .kb/ 데이터를 DB에 업로드")
2532
- .option("--bot <name>", " ID", detectBotId())
2533
- .option("--target <type>", "대상 (shared|bot)", "bot")
2534
- .option("--file <path>", ".kb/ 내 특정 파일")
2261
+ .option("--file <path>", ".kb/ 내 특정 파일", "team.json")
2262
+ .option("--created-by <name>", "작성자 식별자")
2535
2263
  .action(async (options) => {
2536
2264
  const cwd = process.cwd();
2537
2265
  const kbDir = path.join(cwd, ".kb");
@@ -2542,14 +2270,13 @@ kbCmd
2542
2270
  const spinner = (0, ora_1.default)("KB 데이터 업로드 중...").start();
2543
2271
  try {
2544
2272
  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);
2273
+ const filePath = path.join(kbDir, options.file);
2547
2274
  if (!fs.existsSync(filePath)) {
2548
- spinner.fail(`파일을 찾을 수 없습니다: .kb/${filename}`);
2275
+ spinner.fail(`파일을 찾을 수 없습니다: .kb/${options.file}`);
2549
2276
  process.exit(1);
2550
2277
  }
2551
2278
  const entries = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2552
- const result = await (0, kb_1.kbPush)(pool, options.bot, entries, options.target, cwd);
2279
+ const result = await (0, kb_1.kbPush)(pool, entries, options.createdBy, cwd);
2553
2280
  spinner.succeed(`KB push 완료`);
2554
2281
  console.log(chalk_1.default.green(` ✅ ${result.upserted}건 업서트됨`));
2555
2282
  if (result.errors.length > 0) {
@@ -2567,15 +2294,14 @@ kbCmd
2567
2294
  kbCmd
2568
2295
  .command("status")
2569
2296
  .description("KB 동기화 상태 확인")
2570
- .option("--bot <name>", "봇 ID", detectBotId())
2571
- .action(async (options) => {
2297
+ .action(async () => {
2572
2298
  const spinner = (0, ora_1.default)("KB 상태 조회 중...").start();
2573
2299
  try {
2574
2300
  const pool = (0, database_1.getPool)();
2575
- const status = await (0, kb_1.kbStatus)(pool, options.bot);
2301
+ const status = await (0, kb_1.kbStatus)(pool);
2576
2302
  spinner.stop();
2577
2303
  console.log(chalk_1.default.cyan.bold("\n📊 KB 상태\n"));
2578
- console.log(chalk_1.default.white(" 📦 공통 KB (shared)"));
2304
+ console.log(chalk_1.default.white(" 📦 KB (knowledge_base)"));
2579
2305
  console.log(chalk_1.default.gray(` 총 ${status.shared.total}건`));
2580
2306
  for (const [domain, count] of Object.entries(status.shared.domains)) {
2581
2307
  console.log(chalk_1.default.gray(` - ${domain}: ${count}건`));
@@ -2583,17 +2309,6 @@ kbCmd
2583
2309
  if (status.shared.lastUpdated) {
2584
2310
  console.log(chalk_1.default.gray(` 최종 업데이트: ${status.shared.lastUpdated}`));
2585
2311
  }
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
2312
  // Local sync state
2598
2313
  const syncState = readSyncState(process.cwd());
2599
2314
  if (syncState.lastPull || syncState.lastPush) {
@@ -2615,42 +2330,30 @@ kbCmd
2615
2330
  kbCmd
2616
2331
  .command("list")
2617
2332
  .description("KB 항목 목록 조회")
2618
- .option("--bot <name>", "봇 ID", detectBotId())
2619
2333
  .option("--domain <name>", "도메인 필터")
2620
2334
  .option("--limit <n>", "최대 항목 수", "50")
2621
2335
  .option("--format <type>", "출력 형식 (table|json)", "table")
2622
2336
  .action(async (options) => {
2623
2337
  try {
2624
2338
  const pool = (0, database_1.getPool)();
2625
- const result = await (0, kb_1.kbList)(pool, {
2339
+ const entries = await (0, kb_1.kbList)(pool, {
2626
2340
  domain: options.domain,
2627
- botId: options.bot,
2628
2341
  limit: parseInt(options.limit),
2629
2342
  });
2630
2343
  if (options.format === "json") {
2631
- console.log(JSON.stringify(result, null, 2));
2344
+ console.log(JSON.stringify(entries, null, 2));
2632
2345
  }
2633
2346
  else {
2634
2347
  console.log(chalk_1.default.cyan.bold("\n📋 KB 목록\n"));
2635
- if (result.shared.length > 0) {
2636
- console.log(chalk_1.default.white(" 📦 공통 KB"));
2637
- console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
2638
- for (const entry of result.shared) {
2639
- const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
2640
- console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
2641
- console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
2642
- }
2643
- }
2644
- if (result.bot.length > 0) {
2645
- console.log(chalk_1.default.white(`\n 🤖 봇 KB (${options.bot})`));
2348
+ if (entries.length > 0) {
2646
2349
  console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
2647
- for (const entry of result.bot) {
2350
+ for (const entry of entries) {
2648
2351
  const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
2649
2352
  console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
2650
2353
  console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
2651
2354
  }
2652
2355
  }
2653
- if (result.shared.length === 0 && result.bot.length === 0) {
2356
+ else {
2654
2357
  console.log(chalk_1.default.yellow(" KB가 비어있습니다."));
2655
2358
  }
2656
2359
  console.log();
@@ -2666,7 +2369,6 @@ kbCmd
2666
2369
  kbCmd
2667
2370
  .command("search <query>")
2668
2371
  .description("KB 검색 (시맨틱 + 텍스트 하이브리드)")
2669
- .option("--bot <name>", "봇 KB도 검색", detectBotId())
2670
2372
  .option("--domain <name>", "도메인 필터")
2671
2373
  .option("--limit <n>", "최대 결과 수", "10")
2672
2374
  .option("--mode <type>", "검색 모드 (hybrid|semantic|text)", "hybrid")
@@ -2676,7 +2378,6 @@ kbCmd
2676
2378
  const pool = (0, database_1.getPool)();
2677
2379
  const results = await (0, kb_1.kbSearch)(pool, query, {
2678
2380
  domain: options.domain,
2679
- botId: options.bot,
2680
2381
  limit: parseInt(options.limit),
2681
2382
  mode: options.mode,
2682
2383
  });
@@ -2703,46 +2404,9 @@ kbCmd
2703
2404
  process.exit(1);
2704
2405
  }
2705
2406
  });
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
2407
  kbCmd
2743
2408
  .command("embed")
2744
2409
  .description("기존 KB 항목에 임베딩 벡터 생성 (OPENAI_API_KEY 필요)")
2745
- .option("--bot <name>", "봇 KB도 임베딩", detectBotId())
2746
2410
  .option("--domain <name>", "도메인 필터")
2747
2411
  .option("--force", "이미 임베딩된 항목도 재생성")
2748
2412
  .action(async (options) => {
@@ -2755,29 +2419,17 @@ kbCmd
2755
2419
  try {
2756
2420
  const pool = (0, database_1.getPool)();
2757
2421
  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 = [];
2422
+ let sql = "SELECT kb_id, domain, key, content FROM semo.knowledge_base WHERE 1=1";
2423
+ const params = [];
2761
2424
  let pIdx = 1;
2762
2425
  if (!options.force)
2763
- sharedSql += " AND embedding IS NULL";
2426
+ sql += " AND embedding IS NULL";
2764
2427
  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);
2428
+ sql += ` AND domain = $${pIdx++}`;
2429
+ params.push(options.domain);
2778
2430
  }
2779
- const botRows = await client.query(botSql, botParams);
2780
- const total = sharedRows.rows.length + botRows.rows.length;
2431
+ const rows = await client.query(sql, params);
2432
+ const total = rows.rows.length;
2781
2433
  spinner.succeed(`${total}건 임베딩 대상`);
2782
2434
  if (total === 0) {
2783
2435
  console.log(chalk_1.default.green(" 모든 항목이 이미 임베딩되어 있습니다."));
@@ -2787,8 +2439,7 @@ kbCmd
2787
2439
  }
2788
2440
  let done = 0;
2789
2441
  const embedSpinner = (0, ora_1.default)(`임베딩 생성 중... 0/${total}`).start();
2790
- // Process shared KB
2791
- for (const row of sharedRows.rows) {
2442
+ for (const row of rows.rows) {
2792
2443
  const embedding = await (0, kb_1.generateEmbedding)(`${row.key}: ${row.content}`);
2793
2444
  if (embedding) {
2794
2445
  await client.query("UPDATE semo.knowledge_base SET embedding = $1::vector WHERE kb_id = $2", [`[${embedding.join(",")}]`, row.kb_id]);
@@ -2796,15 +2447,6 @@ kbCmd
2796
2447
  done++;
2797
2448
  embedSpinner.text = `임베딩 생성 중... ${done}/${total}`;
2798
2449
  }
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
2450
  embedSpinner.succeed(`${done}건 임베딩 완료`);
2809
2451
  client.release();
2810
2452
  await (0, database_1.closeConnection)();
@@ -2818,7 +2460,6 @@ kbCmd
2818
2460
  kbCmd
2819
2461
  .command("sync")
2820
2462
  .description("양방향 동기화 (pull → merge → push)")
2821
- .option("--bot <name>", "봇 ID", detectBotId())
2822
2463
  .option("--domain <name>", "도메인 필터")
2823
2464
  .action(async (options) => {
2824
2465
  console.log(chalk_1.default.cyan.bold("\n🔄 KB 동기화\n"));
@@ -2826,8 +2467,8 @@ kbCmd
2826
2467
  try {
2827
2468
  const pool = (0, database_1.getPool)();
2828
2469
  // 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}건`);
2470
+ const pulled = await (0, kb_1.kbPull)(pool, options.domain, process.cwd());
2471
+ spinner.succeed(`Pull 완료: ${pulled.length}건`);
2831
2472
  // Step 2: Pull ontology
2832
2473
  const spinner2 = (0, ora_1.default)("Step 2/2: 온톨로지 동기화...").start();
2833
2474
  const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
@@ -2957,6 +2598,7 @@ ontoCmd
2957
2598
  (0, bots_1.registerBotsCommands)(program);
2958
2599
  (0, get_1.registerGetCommands)(program);
2959
2600
  (0, sessions_1.registerSessionsCommands)(program);
2601
+ (0, db_1.registerDbCommands)(program);
2960
2602
  // === semo skills — DB 시딩 ===
2961
2603
  /**
2962
2604
  * SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
@@ -2984,119 +2626,12 @@ function parseSkillFrontmatter(content) {
2984
2626
  // tools 배열
2985
2627
  const toolsMatch = fm.match(/^tools:\s*\[(.+)\]$/m);
2986
2628
  const tools = toolsMatch ? toolsMatch[1].split(",").map((t) => t.trim()) : [];
2987
- // category: 상위 디렉토리명 또는 name 자체 (semo-skills의 경우 dir = skill name)
2629
+ // category
2988
2630
  const category = "core";
2989
2631
  return { name, description, category, tools };
2990
2632
  }
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
- });
2633
+ // semo skills seed — 제거됨 (중앙 DB 단일 SoT)
2634
+ // 전용 스킬은 semo bots seed로 동기화
3100
2635
  // === -v 옵션 처리 (program.parse 전에 직접 처리) ===
3101
2636
  async function main() {
3102
2637
  const args = process.argv.slice(2);