@team-semicolon/semo-cli 4.6.0 → 4.7.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.
@@ -590,15 +590,30 @@ function registerBotsCommands(program) {
590
590
  .option("--format <type>", "출력 형식 (table|json|slack)", "table")
591
591
  .option("--fix", "누락 파일/디렉토리 자동 생성")
592
592
  .option("--no-db", "DB 저장 건너뛰기")
593
+ .option("--local", "~/.claude/semo/bots/ 로컬 미러 audit")
593
594
  .action(async (options) => {
594
595
  const home = process.env.HOME || "/Users/reus";
595
- const spinner = (0, ora_1.default)("bot-workspaces audit 중... (v2.0 SoT: ~/.openclaw-*/workspace/)").start();
596
- // v2.0: SoT는 ~/.openclaw-{bot}/workspace/
596
+ const isLocal = options.local === true;
597
+ const sourceLabel = isLocal ? "~/.claude/semo/bots/" : "~/.openclaw-*/workspace/";
598
+ const spinner = (0, ora_1.default)(`bot-workspaces audit 중... (${sourceLabel})`).start();
597
599
  const botEntries = [];
598
- for (const botId of KNOWN_BOTS) {
599
- const botDir = path.join(home, `.openclaw-${botId}`, "workspace");
600
- if (fs.existsSync(botDir)) {
601
- botEntries.push({ botId, botDir });
600
+ if (isLocal) {
601
+ // 로컬 미러: ~/.claude/semo/bots/{botId}/
602
+ const semoBotsDir = path.join(home, ".claude", "semo", "bots");
603
+ if (fs.existsSync(semoBotsDir)) {
604
+ const dirs = fs.readdirSync(semoBotsDir).filter(f => fs.statSync(path.join(semoBotsDir, f)).isDirectory());
605
+ for (const botId of dirs) {
606
+ botEntries.push({ botId, botDir: path.join(semoBotsDir, botId) });
607
+ }
608
+ }
609
+ }
610
+ else {
611
+ // v2.0: SoT는 ~/.openclaw-{bot}/workspace/
612
+ for (const botId of KNOWN_BOTS) {
613
+ const botDir = path.join(home, `.openclaw-${botId}`, "workspace");
614
+ if (fs.existsSync(botDir)) {
615
+ botEntries.push({ botId, botDir });
616
+ }
602
617
  }
603
618
  }
604
619
  if (botEntries.length === 0) {
@@ -620,7 +635,9 @@ function registerBotsCommands(program) {
620
635
  if (options.fix) {
621
636
  let totalFixed = 0;
622
637
  for (const r of results) {
623
- const botDir = path.join(home, `.openclaw-${r.botId}`, "workspace");
638
+ const botDir = isLocal
639
+ ? path.join(home, ".claude", "semo", "bots", r.botId)
640
+ : path.join(home, `.openclaw-${r.botId}`, "workspace");
624
641
  const fixed = (0, audit_1.fixBot)(botDir, r.botId, r.checks);
625
642
  if (fixed > 0) {
626
643
  console.log(chalk_1.default.green(` ✔ ${r.botId}: ${fixed}개 파일/디렉토리 생성`));
@@ -631,7 +648,9 @@ function registerBotsCommands(program) {
631
648
  console.log(chalk_1.default.green(`\n총 ${totalFixed}개 수정`));
632
649
  // Re-audit after fix
633
650
  for (let i = 0; i < results.length; i++) {
634
- const botDir = path.join(home, `.openclaw-${results[i].botId}`, "workspace");
651
+ const botDir = isLocal
652
+ ? path.join(home, ".claude", "semo", "bots", results[i].botId)
653
+ : path.join(home, `.openclaw-${results[i].botId}`, "workspace");
635
654
  results[i] = (0, audit_1.auditBot)(botDir, results[i].botId);
636
655
  }
637
656
  }
@@ -1039,6 +1058,122 @@ function registerBotsCommands(program) {
1039
1058
  await (0, database_1.closeConnection)();
1040
1059
  }
1041
1060
  });
1061
+ // ── semo bots skill-deploy ──────────────────────────────────
1062
+ botsCmd
1063
+ .command("skill-deploy")
1064
+ .description("DB skill_definitions → 봇 워크스페이스 SKILL.md 역배포")
1065
+ .option("--bot <botId>", "특정 봇에만 배포")
1066
+ .option("--dry-run", "파일 쓰기 없이 계획만 출력")
1067
+ .option("--force", "기존 SKILL.md와 내용이 달라도 덮어쓰기")
1068
+ .action(async (options) => {
1069
+ const spinner = (0, ora_1.default)("스킬 배포 준비 중...").start();
1070
+ const connected = await (0, database_1.isDbConnected)();
1071
+ if (!connected) {
1072
+ spinner.fail("DB 연결 실패");
1073
+ await (0, database_1.closeConnection)();
1074
+ process.exit(1);
1075
+ }
1076
+ try {
1077
+ const skills = await (0, database_1.getActiveSkills)();
1078
+ spinner.succeed(`활성 스킬 ${skills.length}개 조회 완료`);
1079
+ // Filter skills that have bot_ids assigned and content
1080
+ const deployable = skills.filter((s) => s.bot_ids && s.bot_ids.length > 0 && s.content && s.content.trim());
1081
+ if (deployable.length === 0) {
1082
+ console.log(chalk_1.default.yellow("배포 가능한 스킬이 없습니다."));
1083
+ return;
1084
+ }
1085
+ const entries = [];
1086
+ for (const skill of deployable) {
1087
+ const targetBots = options.bot
1088
+ ? skill.bot_ids.filter((b) => b === options.bot)
1089
+ : skill.bot_ids;
1090
+ for (const botId of targetBots) {
1091
+ const wsDir = path.join(os.homedir(), `.openclaw-${botId}`, "workspace");
1092
+ const skillDir = path.join(wsDir, "skills", skill.name);
1093
+ const filePath = path.join(skillDir, "SKILL.md");
1094
+ let action;
1095
+ if (!fs.existsSync(filePath)) {
1096
+ action = "CREATE";
1097
+ }
1098
+ else {
1099
+ const existing = fs.readFileSync(filePath, "utf-8");
1100
+ if (existing === skill.content) {
1101
+ action = "SKIP_SAME";
1102
+ }
1103
+ else if (options.force) {
1104
+ action = "OVERWRITE";
1105
+ }
1106
+ else {
1107
+ action = "SKIP_EXISTS";
1108
+ }
1109
+ }
1110
+ entries.push({ skillName: skill.name, botId, action, filePath, content: skill.content });
1111
+ }
1112
+ }
1113
+ // Summary table
1114
+ const actionColor = {
1115
+ CREATE: chalk_1.default.green,
1116
+ OVERWRITE: chalk_1.default.yellow,
1117
+ SKIP_SAME: chalk_1.default.gray,
1118
+ SKIP_EXISTS: chalk_1.default.cyan,
1119
+ };
1120
+ console.log("\n" + chalk_1.default.bold("배포 계획:"));
1121
+ console.log("─".repeat(70));
1122
+ for (const e of entries) {
1123
+ const tag = actionColor[e.action](e.action.padEnd(12));
1124
+ console.log(` ${tag} ${e.botId}/${e.skillName}`);
1125
+ }
1126
+ console.log("─".repeat(70));
1127
+ const creates = entries.filter((e) => e.action === "CREATE").length;
1128
+ const overwrites = entries.filter((e) => e.action === "OVERWRITE").length;
1129
+ const skipSame = entries.filter((e) => e.action === "SKIP_SAME").length;
1130
+ const skipExists = entries.filter((e) => e.action === "SKIP_EXISTS").length;
1131
+ console.log(` CREATE: ${creates} OVERWRITE: ${overwrites} 동일: ${skipSame} 스킵(--force 필요): ${skipExists}`);
1132
+ if (options.dryRun) {
1133
+ console.log(chalk_1.default.yellow("\n--dry-run: 파일 쓰기를 건너뜁니다."));
1134
+ return;
1135
+ }
1136
+ const toWrite = entries.filter((e) => e.action === "CREATE" || e.action === "OVERWRITE");
1137
+ if (toWrite.length === 0) {
1138
+ console.log(chalk_1.default.green("\n변경할 파일이 없습니다."));
1139
+ return;
1140
+ }
1141
+ // Write files
1142
+ const writeSpinner = (0, ora_1.default)(`SKILL.md ${toWrite.length}개 배포 중...`).start();
1143
+ for (const e of toWrite) {
1144
+ const dir = path.dirname(e.filePath);
1145
+ fs.mkdirSync(dir, { recursive: true });
1146
+ fs.writeFileSync(e.filePath, e.content, "utf-8");
1147
+ }
1148
+ writeSpinner.succeed(`SKILL.md ${toWrite.length}개 배포 완료`);
1149
+ // Sync workspace files for affected bots
1150
+ const affectedBots = [...new Set(toWrite.map((e) => e.botId))];
1151
+ const pool = (0, database_1.getPool)();
1152
+ const client = await pool.connect();
1153
+ try {
1154
+ for (const botId of affectedBots) {
1155
+ const wsDir = path.join(os.homedir(), `.openclaw-${botId}`, "workspace");
1156
+ if (fs.existsSync(wsDir)) {
1157
+ const syncSpinner = (0, ora_1.default)(`${botId} 워크스페이스 DB 싱크 중...`).start();
1158
+ const count = await syncWorkspaceFiles(client, botId, wsDir);
1159
+ syncSpinner.succeed(`${botId} 워크스페이스 싱크 완료 (${count}개 파일 갱신)`);
1160
+ }
1161
+ }
1162
+ }
1163
+ finally {
1164
+ client.release();
1165
+ }
1166
+ console.log(chalk_1.default.green(`\n✔ 스킬 역배포 완료`));
1167
+ }
1168
+ catch (err) {
1169
+ spinner.isSpinning && spinner.fail("스킬 배포 실패");
1170
+ console.error(chalk_1.default.red(`❌ ${err}`));
1171
+ process.exit(1);
1172
+ }
1173
+ finally {
1174
+ await (0, database_1.closeConnection)();
1175
+ }
1176
+ });
1042
1177
  // ── semo bots set-status ─────────────────────────────────────
1043
1178
  botsCmd
1044
1179
  .command("set-status <bot_id> <status>")
@@ -55,6 +55,7 @@ const database_1 = require("../database");
55
55
  const kb_1 = require("../kb");
56
56
  // [v4.4.0] syncSkillsToDB 제거 — semo-system/ 폐기됨, 스킬 SoT는 DB 직접 관리
57
57
  const global_cache_1 = require("../global-cache");
58
+ const semo_workspace_1 = require("../semo-workspace");
58
59
  // ============================================================
59
60
  // Memory file mapping
60
61
  // ============================================================
@@ -224,7 +225,21 @@ function registerContextCommands(program) {
224
225
  console.log(chalk_1.default.yellow(` ⚠ 글로벌 캐시 동기화 실패 (기존 파일 유지): ${cacheErr}`));
225
226
  }
226
227
  }
227
- // 3. 크론잡 동기화 (localDB)
228
+ // 3. 미러 리프레시 (DB~/.claude/semo/bots/)
229
+ const semoDir = path.join(os.homedir(), ".claude", "semo");
230
+ if (fs.existsSync(semoDir)) {
231
+ try {
232
+ spinner.text = "봇 미러 동기화 (~/.claude/semo/bots/)...";
233
+ const mirrorResult = await (0, semo_workspace_1.populateBotMirrors)();
234
+ if (mirrorResult.files > 0) {
235
+ console.log(chalk_1.default.green(` ✓ 봇 미러: ${mirrorResult.bots}개 봇, ${mirrorResult.files}개 파일`));
236
+ }
237
+ }
238
+ catch {
239
+ // 봇 미러 동기화 실패는 비치명적
240
+ }
241
+ }
242
+ // 4. 크론잡 동기화 (local → DB)
228
243
  try {
229
244
  spinner.text = "크론잡 동기화...";
230
245
  const cronResult = await syncCronJobs(pool);
@@ -235,7 +250,7 @@ function registerContextCommands(program) {
235
250
  catch {
236
251
  // 크론잡 동기화 실패는 비치명적
237
252
  }
238
- spinner.succeed("context sync 완료 — 스킬/캐시/크론잡 동기화");
253
+ spinner.succeed("context sync 완료 — 스킬/캐시/봇미러/크론잡 동기화");
239
254
  console.log(chalk_1.default.gray(` 저장 위치: ${memDir}`));
240
255
  }
241
256
  catch (err) {
@@ -211,6 +211,9 @@ function registerMemoryCommands(program) {
211
211
  .option("--dry-run", "프리뷰만 (실제 동기화 안 함)")
212
212
  .option("--force", "워터마크 무시, 전체 재동기화")
213
213
  .action(async (options) => {
214
+ console.log(chalk_1.default.yellow.bold("\n⚠️ [DEPRECATED] semo memory sync는 semiclaw memory-escalation 크론잡으로 대체되었습니다."));
215
+ console.log(chalk_1.default.yellow(" 매일 06:00 자동 실행되며, LLM 기반 분류로 적절한 KB 도메인/키에 에스컬레이션합니다."));
216
+ console.log(chalk_1.default.yellow(" 수동 실행이 필요하면 semiclaw에게 'memory-escalation 스킬 실행' 을 지시하세요.\n"));
214
217
  const dryRun = !!options.dryRun;
215
218
  const force = !!options.force;
216
219
  const minAgeDays = parseInt(options.days) || 2;
@@ -123,3 +123,15 @@ export declare function closeConnection(): Promise<void>;
123
123
  * DB 연결 상태 확인
124
124
  */
125
125
  export declare function isDbConnected(): Promise<boolean>;
126
+ export interface BotWorkspaceFile {
127
+ file_path: string;
128
+ content: string;
129
+ }
130
+ /**
131
+ * 활성 봇 ID 목록 조회 (retired 제외)
132
+ */
133
+ export declare function getActiveBotIds(): Promise<string[]>;
134
+ /**
135
+ * 특정 봇의 워크스페이스 파일 목록 조회 (DB 미러)
136
+ */
137
+ export declare function getBotWorkspaceFiles(botId: string): Promise<BotWorkspaceFile[]>;
package/dist/database.js CHANGED
@@ -58,16 +58,21 @@ exports.getDelegations = getDelegations;
58
58
  exports.getProtocol = getProtocol;
59
59
  exports.closeConnection = closeConnection;
60
60
  exports.isDbConnected = isDbConnected;
61
+ exports.getActiveBotIds = getActiveBotIds;
62
+ exports.getBotWorkspaceFiles = getBotWorkspaceFiles;
61
63
  const pg_1 = require("pg");
62
64
  const fs = __importStar(require("fs"));
63
65
  const os = __importStar(require("os"));
64
66
  const path = __importStar(require("path"));
65
67
  const env_parser_1 = require("./env-parser");
66
- // ~/.semo.env 자동 로드 — LaunchAgent / Claude Code 앱 / cron 등
68
+ // ~/.claude/semo/.env 자동 로드 — LaunchAgent / Claude Code 앱 / cron 등
67
69
  // 인터랙티브 쉘이 아닌 환경에서 환경변수를 공급한다.
68
70
  // 이미 설정된 환경변수는 덮어쓰지 않는다 (env var > file).
71
+ // v4.5.0: ~/.semo.env → ~/.claude/semo/.env 이전. 구 경로 폴백 유지.
69
72
  function loadSemoEnv() {
70
- const envFile = path.join(os.homedir(), ".semo.env");
73
+ const newEnvFile = path.join(os.homedir(), ".claude", "semo", ".env");
74
+ const legacyEnvFile = path.join(os.homedir(), ".semo.env");
75
+ const envFile = fs.existsSync(newEnvFile) ? newEnvFile : legacyEnvFile;
71
76
  if (!fs.existsSync(envFile))
72
77
  return;
73
78
  try {
@@ -382,3 +387,33 @@ async function closeConnection() {
382
387
  async function isDbConnected() {
383
388
  return checkDbConnection();
384
389
  }
390
+ /**
391
+ * 활성 봇 ID 목록 조회 (retired 제외)
392
+ */
393
+ async function getActiveBotIds() {
394
+ const isConnected = await checkDbConnection();
395
+ if (!isConnected)
396
+ return [];
397
+ try {
398
+ const result = await getPool().query(`SELECT bot_id FROM semo.bot_status WHERE status != 'retired' ORDER BY bot_id`);
399
+ return result.rows.map((r) => r.bot_id);
400
+ }
401
+ catch {
402
+ return [];
403
+ }
404
+ }
405
+ /**
406
+ * 특정 봇의 워크스페이스 파일 목록 조회 (DB 미러)
407
+ */
408
+ async function getBotWorkspaceFiles(botId) {
409
+ const isConnected = await checkDbConnection();
410
+ if (!isConnected)
411
+ return [];
412
+ try {
413
+ const result = await getPool().query(`SELECT file_path, content FROM semo.bot_workspace_files WHERE bot_id = $1 ORDER BY file_path`, [botId]);
414
+ return result.rows;
415
+ }
416
+ catch {
417
+ return [];
418
+ }
419
+ }
package/dist/index.js CHANGED
@@ -66,6 +66,7 @@ const db_1 = require("./commands/db");
66
66
  const memory_1 = require("./commands/memory");
67
67
  const test_1 = require("./commands/test");
68
68
  const global_cache_1 = require("./global-cache");
69
+ const semo_workspace_1 = require("./semo-workspace");
69
70
  const PACKAGE_NAME = "@team-semicolon/semo-cli";
70
71
  // package.json에서 버전 동적 로드
71
72
  function getCliVersion() {
@@ -744,20 +745,24 @@ async function showToolsStatus() {
744
745
  // === 글로벌 설정 체크 ===
745
746
  function isGlobalSetupDone() {
746
747
  const home = os.homedir();
747
- return (fs.existsSync(path.join(home, ".semo.env")) &&
748
- fs.existsSync(path.join(home, ".claude", "skills")));
748
+ const hasEnv = fs.existsSync(path.join(home, ".claude", "semo", ".env")) ||
749
+ fs.existsSync(path.join(home, ".semo.env")); // 하위 호환
750
+ const hasSetup = fs.existsSync(path.join(home, ".claude", "semo", "SOUL.md")) ||
751
+ fs.existsSync(path.join(home, ".claude", "skills")); // 하위 호환
752
+ return hasEnv && hasSetup;
749
753
  }
750
- // === onboarding 명령어 (글로벌 1회 설정) ===
754
+ // === onboarding 명령어 (글로벌 설정 — init 통합) ===
751
755
  program
752
756
  .command("onboarding")
753
- .description("글로벌 SEMO 설정 (머신당 1회) — ~/.claude/, ~/.semo.env")
757
+ .description("글로벌 SEMO 설정 — ~/.claude/semo/, skills/agents/commands")
754
758
  .option("--credentials-gist <gistId>", "Private GitHub Gist에서 DB 접속정보 가져오기")
755
759
  .option("-f, --force", "기존 설정 덮어쓰기")
756
760
  .option("--skip-mcp", "MCP 설정 생략")
761
+ .option("--skip-bots", "봇 워크스페이스 미러 건너뛰기")
757
762
  .action(async (options) => {
758
- console.log(chalk_1.default.cyan.bold("\n🏠 SEMO 글로벌 온보딩\n"));
759
- console.log(chalk_1.default.gray(" 대상: ~/.claude/, ~/.semo.env (머신당 1회)\n"));
760
- // 1. ~/.semo.env DB 접속 설정
763
+ console.log(chalk_1.default.cyan.bold("\n🏠 SEMO 온보딩\n"));
764
+ console.log(chalk_1.default.gray(" 대상: ~/.claude/semo/ (머신당 1회)\n"));
765
+ // 1. ~/.claude/semo/.env DB 접속 설정
761
766
  await setupSemoEnv(options.credentialsGist, options.force);
762
767
  // 2. DB health check
763
768
  const spinner = (0, ora_1.default)("DB 연결 확인 중...").start();
@@ -766,103 +771,82 @@ program
766
771
  spinner.succeed("DB 연결 확인됨");
767
772
  }
768
773
  else {
769
- spinner.warn("DB 연결 실패 — 스킬/커맨드/에이전트 설치를 건너뜁니다");
770
- console.log(chalk_1.default.gray(" ~/.semo.env를 확인하고 다시 시도하세요: semo onboarding\n"));
774
+ spinner.warn("DB 연결 실패 — 스킬/봇 미러 설치를 건너뜁니다");
775
+ console.log(chalk_1.default.gray(" ~/.claude/semo/.env를 확인하고 다시 시도하세요: semo onboarding\n"));
771
776
  await (0, database_1.closeConnection)();
772
777
  return;
773
778
  }
774
- // 3. Standard 설치 (DB → ~/.claude/skills, commands, agents)
779
+ // 3. ~/.claude/semo/ 디렉토리 구조 생성
780
+ console.log(chalk_1.default.cyan("\n📂 SEMO 워크스페이스 구성 (~/.claude/semo/)"));
781
+ (0, semo_workspace_1.ensureSemoDir)();
782
+ console.log(chalk_1.default.green(" ✓ ~/.claude/semo/ 디렉토리 생성됨"));
783
+ // 4. 봇 워크스페이스 미러 (DB → semo/bots/)
784
+ if (!options.skipBots) {
785
+ const mirrorSpinner = (0, ora_1.default)("봇 워크스페이스 미러링 (DB → semo/bots/)...").start();
786
+ try {
787
+ const result = await (0, semo_workspace_1.populateBotMirrors)();
788
+ mirrorSpinner.succeed(`봇 미러 완료: ${result.bots}개 봇, ${result.files}개 파일`);
789
+ }
790
+ catch (err) {
791
+ mirrorSpinner.warn(`봇 미러 실패 (계속 진행): ${err}`);
792
+ }
793
+ }
794
+ else {
795
+ console.log(chalk_1.default.gray(" → 봇 미러 건너뜀 (--skip-bots)"));
796
+ }
797
+ // 5. SOUL.md / MEMORY.md / USER.md 생성
798
+ try {
799
+ await (0, semo_workspace_1.generateSoulMd)();
800
+ console.log(chalk_1.default.green(" ✓ semo/SOUL.md 생성됨 (오케스트레이터 페르소나)"));
801
+ }
802
+ catch (err) {
803
+ console.log(chalk_1.default.yellow(` ⚠ SOUL.md 생성 실패: ${err}`));
804
+ }
805
+ (0, semo_workspace_1.generateMemoryMd)();
806
+ console.log(chalk_1.default.green(" ✓ semo/MEMORY.md 생성됨 (KB 인덱스)"));
807
+ (0, semo_workspace_1.generateUserMd)();
808
+ console.log(chalk_1.default.green(" ✓ semo/USER.md 확인됨 (사용자 프로필)"));
809
+ // 6. Standard 설치 (DB → ~/.claude/skills, commands, agents)
775
810
  await setupStandardGlobal();
776
- // 4. Hooks 설치 (프로젝트 무관)
811
+ // 7. Hooks 설치
777
812
  await setupHooks(false);
778
- // 5. MCP 설정 (글로벌 공통 서버)
813
+ // 8. MCP 설정
779
814
  if (!options.skipMcp) {
780
815
  await setupMCP(os.homedir(), [], options.force || false);
781
816
  }
782
- // 6. 글로벌 CLAUDE.md KB-First 규칙 주입
783
- await injectKbFirstToGlobalClaudeMd();
817
+ // 9. Thin Router CLAUDE.md 생성
818
+ console.log(chalk_1.default.cyan("\n📄 CLAUDE.md 라우터 생성"));
819
+ const kbFirstBlock = await buildKbFirstBlock();
820
+ (0, semo_workspace_1.generateThinRouter)(kbFirstBlock);
821
+ console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md (thin router) 생성됨"));
784
822
  await (0, database_1.closeConnection)();
785
823
  // 결과 요약
786
- console.log(chalk_1.default.green.bold("\n✅ SEMO 글로벌 온보딩 완료!\n"));
824
+ console.log(chalk_1.default.green.bold("\n✅ SEMO 온보딩 완료!\n"));
787
825
  console.log(chalk_1.default.cyan("설치된 구성:"));
788
- console.log(chalk_1.default.gray(" ~/.semo.env DB 접속정보 (권한 600)"));
789
- console.log(chalk_1.default.gray(" ~/.claude/skills/ 스킬 (DB 기반)"));
790
- console.log(chalk_1.default.gray(" ~/.claude/commands/ 커맨드 (DB 기반)"));
791
- console.log(chalk_1.default.gray(" ~/.claude/agents/ 에이전트 (DB 기반, dedup)"));
826
+ console.log(chalk_1.default.gray(" ~/.claude/semo/.env DB 접속정보 (권한 600)"));
827
+ console.log(chalk_1.default.gray(" ~/.claude/semo/SOUL.md 오케스트레이터 페르소나"));
828
+ console.log(chalk_1.default.gray(" ~/.claude/semo/MEMORY.md KB 접근 가이드"));
829
+ console.log(chalk_1.default.gray(" ~/.claude/semo/USER.md 사용자 프로필"));
830
+ console.log(chalk_1.default.gray(" ~/.claude/semo/bots/ 봇 워크스페이스 미러"));
831
+ console.log(chalk_1.default.gray(" ~/.claude/skills/ 팀 스킬 (DB 기반)"));
832
+ console.log(chalk_1.default.gray(" ~/.claude/commands/ 팀 커맨드 (DB 기반)"));
833
+ console.log(chalk_1.default.gray(" ~/.claude/agents/ 팀 에이전트 (DB 기반)"));
834
+ console.log(chalk_1.default.gray(" ~/.claude/CLAUDE.md Thin router + KB-First"));
792
835
  console.log(chalk_1.default.gray(" ~/.claude/settings.local.json SessionStart/Stop 훅"));
793
- console.log(chalk_1.default.gray(" ~/.claude/settings.json Claude Code 설정 (유저레벨)"));
794
836
  console.log(chalk_1.default.cyan("\n다음 단계:"));
795
- console.log(chalk_1.default.gray(" 프로젝트 디렉토리에서 'semo init'을 실행하세요."));
837
+ console.log(chalk_1.default.gray(" Claude Code에서 프로젝트를 열면 SessionStart 훅이 자동으로 sync합니다."));
796
838
  console.log();
797
839
  });
798
- // === init 명령어 (프로젝트별 설정) ===
840
+ // === init 명령어 (deprecated — onboarding으로 통합됨) ===
799
841
  program
800
842
  .command("init")
801
- .description("현재 프로젝트에 SEMO 프로젝트 설정을 합니다 (글로벌: semo onboarding)")
802
- .option("-f, --force", "기존 설정 덮어쓰기")
803
- .option("--no-gitignore", ".gitignore 수정 생략")
804
- .action(async (options) => {
805
- console.log(chalk_1.default.cyan.bold("\n📁 SEMO 프로젝트 설정\n"));
806
- const cwd = process.cwd();
807
- // 0. 글로벌 설정 확인
808
- if (!isGlobalSetupDone()) {
809
- console.log(chalk_1.default.yellow("⚠ 글로벌 설정이 완료되지 않았습니다."));
810
- console.log(chalk_1.default.gray(" 먼저 'semo onboarding'을 실행하세요.\n"));
811
- console.log(chalk_1.default.gray(" 이 머신에서 처음 SEMO를 사용하시나요?"));
812
- console.log(chalk_1.default.cyan(" → semo onboarding --credentials-gist <GIST_ID>\n"));
813
- process.exit(1);
814
- }
815
- // 1. Git 레포지토리 확인
816
- const spinner = (0, ora_1.default)("Git 레포지토리 확인 중...").start();
817
- try {
818
- (0, child_process_1.execSync)("git rev-parse --git-dir", { cwd, stdio: "pipe" });
819
- spinner.succeed("Git 레포지토리 확인됨");
820
- }
821
- catch {
822
- spinner.fail("Git 레포지토리가 아닙니다. 'git init'을 먼저 실행하세요.");
823
- process.exit(1);
824
- }
825
- // 2. .claude 디렉토리 생성
826
- const claudeDir = path.join(cwd, ".claude");
827
- if (!fs.existsSync(claudeDir)) {
828
- fs.mkdirSync(claudeDir, { recursive: true });
829
- console.log(chalk_1.default.green("\n✓ .claude/ 디렉토리 생성됨"));
830
- }
831
- // 3. 로컬 스킬 경고 (기존 프로젝트 호환)
832
- const localSkillsDir = path.join(claudeDir, "skills");
833
- if (fs.existsSync(localSkillsDir)) {
834
- try {
835
- const localSkills = fs.readdirSync(localSkillsDir).filter(f => fs.statSync(path.join(localSkillsDir, f)).isDirectory());
836
- if (localSkills.length > 0) {
837
- console.log(chalk_1.default.yellow(`\nℹ 프로젝트 로컬 스킬이 감지되었습니다 (${localSkills.length}개).`));
838
- console.log(chalk_1.default.gray(" 글로벌 스킬(~/.claude/skills/)이 우선 적용됩니다."));
839
- console.log(chalk_1.default.gray(" 로컬 스킬을 제거하려면: rm -rf .claude/skills/ .claude/commands/ .claude/agents/\n"));
840
- }
841
- }
842
- catch {
843
- // ignore
844
- }
845
- }
846
- // 4. Context Mesh 초기화
847
- await setupContextMesh(cwd);
848
- // 5. CLAUDE.md 생성 (프로젝트 규칙만, 스킬 목록 없음)
849
- await setupClaudeMd(cwd, [], options.force || false);
850
- // 6. .gitignore 업데이트
851
- if (options.gitignore !== false) {
852
- updateGitignore(cwd);
853
- }
854
- // 완료 메시지
855
- console.log(chalk_1.default.green.bold("\n✅ SEMO 프로젝트 설정 완료!\n"));
856
- console.log(chalk_1.default.cyan("생성된 파일:"));
857
- console.log(chalk_1.default.gray(" {cwd}/.claude/CLAUDE.md 프로젝트 규칙"));
858
- console.log(chalk_1.default.gray(" {cwd}/.claude/memory/context.md 프로젝트 상태"));
859
- console.log(chalk_1.default.gray(" {cwd}/.claude/memory/decisions.md ADR"));
860
- console.log(chalk_1.default.gray(" {cwd}/.claude/memory/projects.md 프로젝트 맵"));
861
- console.log(chalk_1.default.cyan("\n다음 단계:"));
862
- console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기 (SessionStart 훅이 자동 sync)"));
863
- console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
864
- console.log(chalk_1.default.gray(" 3. /SEMO:help로 도움말 확인"));
865
- console.log();
843
+ .description("[deprecated] semo onboarding으로 통합되었습니다")
844
+ .action(async () => {
845
+ console.log(chalk_1.default.yellow("\n⚠ 'semo init'은 'semo onboarding'으로 통합되었습니다.\n"));
846
+ console.log(chalk_1.default.cyan(" 글로벌 설정이 필요하면:"));
847
+ console.log(chalk_1.default.gray(" semo onboarding\n"));
848
+ console.log(chalk_1.default.cyan(" 이미 온보딩을 완료했다면:"));
849
+ console.log(chalk_1.default.gray(" Claude Code에서 프로젝트를 열면 SessionStart 훅이 자동으로 sync합니다.\n"));
866
850
  });
867
851
  // === Standard 설치 (DB 기반, 글로벌 ~/.claude/) ===
868
852
  async function setupStandardGlobal() {
@@ -1186,6 +1170,10 @@ const BASE_MCP_SERVERS = [
1186
1170
  scope: "user",
1187
1171
  },
1188
1172
  ];
1173
+ // === ~/.claude/semo/.env 설정 (자동 감지 → Gist → 프롬프트) ===
1174
+ // v4.5.0: ~/.semo.env → ~/.claude/semo/.env 이전
1175
+ const SEMO_ENV_PATH = path.join(os.homedir(), ".claude", "semo", ".env");
1176
+ const LEGACY_ENV_PATH = path.join(os.homedir(), ".semo.env");
1189
1177
  const SEMO_CREDENTIALS = [
1190
1178
  {
1191
1179
  key: "DATABASE_URL",
@@ -1210,10 +1198,12 @@ const SEMO_CREDENTIALS = [
1210
1198
  },
1211
1199
  ];
1212
1200
  function writeSemoEnvFile(creds) {
1213
- const envFile = path.join(os.homedir(), ".semo.env");
1201
+ // 디렉토리 보장
1202
+ fs.mkdirSync(path.dirname(SEMO_ENV_PATH), { recursive: true });
1214
1203
  const lines = [
1215
1204
  "# SEMO 환경변수 — 모든 컨텍스트에서 자동 로드됨",
1216
1205
  "# (Claude Code 앱, OpenClaw LaunchAgent, cron 등)",
1206
+ "# 경로: ~/.claude/semo/.env (v4.5.0+)",
1217
1207
  "",
1218
1208
  ];
1219
1209
  // 레지스트리 키 먼저 (순서 보장)
@@ -1230,10 +1220,28 @@ function writeSemoEnvFile(creds) {
1230
1220
  }
1231
1221
  }
1232
1222
  lines.push("");
1233
- fs.writeFileSync(envFile, lines.join("\n"), { mode: 0o600 });
1223
+ fs.writeFileSync(SEMO_ENV_PATH, lines.join("\n"), { mode: 0o600 });
1224
+ // 하위 호환 심링크: ~/.semo.env → ~/.claude/semo/.env
1225
+ try {
1226
+ if (fs.existsSync(LEGACY_ENV_PATH)) {
1227
+ const stat = fs.lstatSync(LEGACY_ENV_PATH);
1228
+ if (!stat.isSymbolicLink()) {
1229
+ // 기존 실파일은 백업 후 심링크로 교체
1230
+ fs.renameSync(LEGACY_ENV_PATH, LEGACY_ENV_PATH + ".bak");
1231
+ }
1232
+ else {
1233
+ fs.unlinkSync(LEGACY_ENV_PATH);
1234
+ }
1235
+ }
1236
+ fs.symlinkSync(SEMO_ENV_PATH, LEGACY_ENV_PATH);
1237
+ }
1238
+ catch {
1239
+ // 심링크 실패 시 무시 — 새 경로가 원본
1240
+ }
1234
1241
  }
1235
1242
  function readSemoEnvCreds() {
1236
- const envFile = path.join(os.homedir(), ".semo.env");
1243
+ // 경로 우선, 없으면 레거시 폴백
1244
+ const envFile = fs.existsSync(SEMO_ENV_PATH) ? SEMO_ENV_PATH : LEGACY_ENV_PATH;
1237
1245
  if (!fs.existsSync(envFile))
1238
1246
  return {};
1239
1247
  try {
@@ -1304,17 +1312,16 @@ async function setupSemoEnv(credentialsGist, force) {
1304
1312
  }
1305
1313
  }
1306
1314
  // 5. 변경사항이 있거나 파일이 없으면 쓰기
1307
- const envFile = path.join(os.homedir(), ".semo.env");
1308
1315
  const needsWrite = force ||
1309
1316
  hasNewKeys ||
1310
- !fs.existsSync(envFile) ||
1317
+ !fs.existsSync(SEMO_ENV_PATH) ||
1311
1318
  Object.keys(gistCreds).some((k) => !existing[k]);
1312
1319
  if (needsWrite) {
1313
1320
  writeSemoEnvFile(merged);
1314
- console.log(chalk_1.default.green(" ✅ ~/.semo.env 저장됨 (권한: 600)"));
1321
+ console.log(chalk_1.default.green(" ✅ ~/.claude/semo/.env 저장됨 (권한: 600)"));
1315
1322
  }
1316
1323
  else {
1317
- console.log(chalk_1.default.gray(" ~/.semo.env 변경 없음"));
1324
+ console.log(chalk_1.default.gray(" ~/.claude/semo/.env 변경 없음"));
1318
1325
  }
1319
1326
  }
1320
1327
  // === Claude MCP 서버 존재 여부 확인 ===
@@ -1441,30 +1448,7 @@ KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하
1441
1448
  **금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
1442
1449
  `;
1443
1450
  }
1444
- async function injectKbFirstToGlobalClaudeMd() {
1445
- const globalClaudeMd = path.join(os.homedir(), ".claude", "CLAUDE.md");
1446
- const kbFirstBlock = await buildKbFirstBlock();
1447
- if (fs.existsSync(globalClaudeMd)) {
1448
- const content = fs.readFileSync(globalClaudeMd, "utf-8");
1449
- if (content.includes(KB_FIRST_SECTION_MARKER)) {
1450
- // 기존 섹션 교체
1451
- const regex = new RegExp(`\\n${KB_FIRST_SECTION_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?(?=\\n## |$)`, "m");
1452
- const updated = content.replace(regex, kbFirstBlock);
1453
- fs.writeFileSync(globalClaudeMd, updated);
1454
- console.log(chalk_1.default.gray(" ~/.claude/CLAUDE.md KB-First 규칙 업데이트됨"));
1455
- }
1456
- else {
1457
- // 끝에 추가
1458
- fs.writeFileSync(globalClaudeMd, content.trimEnd() + "\n" + kbFirstBlock);
1459
- console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md에 KB-First 규칙 추가됨"));
1460
- }
1461
- }
1462
- else {
1463
- // 파일 없으면 생성
1464
- fs.writeFileSync(globalClaudeMd, kbFirstBlock.trim() + "\n");
1465
- console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md 생성됨 (KB-First 규칙)"));
1466
- }
1467
- }
1451
+ // injectKbFirstToGlobalClaudeMd 제거 — generateThinRouter() 대체 (semo-workspace.ts)
1468
1452
  // === MCP 설정 ===
1469
1453
  async function setupMCP(cwd, _extensions, force) {
1470
1454
  console.log(chalk_1.default.cyan("\n🔧 Black Box 설정 (MCP Server)"));
@@ -1530,44 +1514,7 @@ async function setupMCP(cwd, _extensions, force) {
1530
1514
  console.log();
1531
1515
  }
1532
1516
  }
1533
- // === .gitignore 업데이트 ===
1534
- function updateGitignore(cwd) {
1535
- console.log(chalk_1.default.cyan("\n📝 .gitignore 업데이트"));
1536
- const gitignorePath = path.join(cwd, ".gitignore");
1537
- const semoIgnoreBlock = `
1538
- # === SEMO ===
1539
- .claude/*
1540
- !.claude/memory/
1541
- !.claude/memory/**
1542
- semo-system/
1543
- `;
1544
- if (fs.existsSync(gitignorePath)) {
1545
- let content = fs.readFileSync(gitignorePath, "utf-8");
1546
- // 이미 SEMO 블록이 있으면 스킵
1547
- if (content.includes("# === SEMO ===")) {
1548
- console.log(chalk_1.default.gray(" → SEMO 블록 이미 존재 (건너뜀)"));
1549
- return;
1550
- }
1551
- // 기존에 .claude/ 또는 .claude 전체 무시 항목 제거 (memory/ 접근을 위해)
1552
- const lines = content.split("\n");
1553
- const filtered = lines.filter(line => {
1554
- const trimmed = line.trim();
1555
- return trimmed !== ".claude" && trimmed !== ".claude/" && trimmed !== ".claude/**";
1556
- });
1557
- if (filtered.length !== lines.length) {
1558
- content = filtered.join("\n");
1559
- console.log(chalk_1.default.gray(" → 기존 .claude 무시 항목 제거됨 (memory/ 접근 허용)"));
1560
- }
1561
- // 기존 파일에 추가
1562
- fs.writeFileSync(gitignorePath, content + semoIgnoreBlock);
1563
- console.log(chalk_1.default.green("✓ .gitignore에 SEMO 규칙 추가됨"));
1564
- }
1565
- else {
1566
- // 새로 생성
1567
- fs.writeFileSync(gitignorePath, semoIgnoreBlock.trim() + "\n");
1568
- console.log(chalk_1.default.green("✓ .gitignore 생성됨 (SEMO 규칙 포함)"));
1569
- }
1570
- }
1517
+ // updateGitignore 제거 — init 통합으로 프로젝트별 .gitignore 수정 불필요
1571
1518
  // === Hooks 설치/업데이트 ===
1572
1519
  async function setupHooks(isUpdate = false) {
1573
1520
  const action = isUpdate ? "업데이트" : "설치";
@@ -1583,7 +1530,7 @@ async function setupHooks(isUpdate = false) {
1583
1530
  hooks: [
1584
1531
  {
1585
1532
  type: "command",
1586
- command: ". ~/.semo.env 2>/dev/null; semo context sync 2>/dev/null || true",
1533
+ command: ". ~/.claude/semo/.env 2>/dev/null || . ~/.semo.env 2>/dev/null; semo context sync 2>/dev/null || true",
1587
1534
  timeout: 30,
1588
1535
  },
1589
1536
  ],
@@ -1595,7 +1542,7 @@ async function setupHooks(isUpdate = false) {
1595
1542
  hooks: [
1596
1543
  {
1597
1544
  type: "command",
1598
- command: ". ~/.semo.env 2>/dev/null; semo context push 2>/dev/null || true",
1545
+ command: ". ~/.claude/semo/.env 2>/dev/null || . ~/.semo.env 2>/dev/null; semo context push 2>/dev/null || true",
1599
1546
  timeout: 30,
1600
1547
  },
1601
1548
  ],
@@ -1961,7 +1908,7 @@ program
1961
1908
  console.log(chalk_1.default.gray(" 대상: ~/.claude/skills, commands, agents (DB 최신)\n"));
1962
1909
  const connected = await (0, database_1.isDbConnected)();
1963
1910
  if (!connected) {
1964
- console.log(chalk_1.default.red(" DB 연결 실패 — ~/.semo.env를 확인하세요."));
1911
+ console.log(chalk_1.default.red(" DB 연결 실패 — ~/.claude/semo/.env를 확인하세요."));
1965
1912
  await (0, database_1.closeConnection)();
1966
1913
  process.exit(1);
1967
1914
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * semo-workspace — ~/.claude/semo/ 디렉토리 관리
3
+ *
4
+ * OpenClaw 봇 워크스페이스 스키마를 로컬에 미러링하여
5
+ * 로컬 Claude Code 세션이 봇 환경과 동형(isomorphic) 구조로 동작하게 한다.
6
+ *
7
+ * v4.5.0: onboarding/init 통합 — 글로벌 단일 설정
8
+ */
9
+ /**
10
+ * ~/.claude/semo/ 디렉토리 트리 생성
11
+ */
12
+ export declare function ensureSemoDir(): void;
13
+ /**
14
+ * DB bot_workspace_files → ~/.claude/semo/bots/{botId}/ 미러링
15
+ *
16
+ * 1. getActiveBotIds() → 봇 목록
17
+ * 2. 봇별 getBotWorkspaceFiles() → 파일 쓰기
18
+ * 3. DB에 없는 orphan 파일 정리
19
+ */
20
+ export declare function populateBotMirrors(): Promise<{
21
+ bots: number;
22
+ files: number;
23
+ }>;
24
+ /**
25
+ * 로컬 오케스트레이터 SOUL.md 생성
26
+ * (봇 SOUL.md와 동일 포맷 — bot_workspace_standard 준수)
27
+ */
28
+ export declare function generateSoulMd(): Promise<void>;
29
+ /**
30
+ * KB 접근 가이드 인덱스
31
+ */
32
+ export declare function generateMemoryMd(): void;
33
+ /**
34
+ * 사용자 프로필 플레이스홀더
35
+ */
36
+ export declare function generateUserMd(): void;
37
+ /**
38
+ * ~/.claude/CLAUDE.md를 얇은 라우터로 생성/교체
39
+ *
40
+ * @param kbFirstBlock - buildKbFirstBlock()의 결과 (index.ts에서 전달)
41
+ */
42
+ export declare function generateThinRouter(kbFirstBlock: string): void;
@@ -0,0 +1,339 @@
1
+ "use strict";
2
+ /**
3
+ * semo-workspace — ~/.claude/semo/ 디렉토리 관리
4
+ *
5
+ * OpenClaw 봇 워크스페이스 스키마를 로컬에 미러링하여
6
+ * 로컬 Claude Code 세션이 봇 환경과 동형(isomorphic) 구조로 동작하게 한다.
7
+ *
8
+ * v4.5.0: onboarding/init 통합 — 글로벌 단일 설정
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ var __importDefault = (this && this.__importDefault) || function (mod) {
44
+ return (mod && mod.__esModule) ? mod : { "default": mod };
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.ensureSemoDir = ensureSemoDir;
48
+ exports.populateBotMirrors = populateBotMirrors;
49
+ exports.generateSoulMd = generateSoulMd;
50
+ exports.generateMemoryMd = generateMemoryMd;
51
+ exports.generateUserMd = generateUserMd;
52
+ exports.generateThinRouter = generateThinRouter;
53
+ const fs = __importStar(require("fs"));
54
+ const path = __importStar(require("path"));
55
+ const os = __importStar(require("os"));
56
+ const chalk_1 = __importDefault(require("chalk"));
57
+ const database_1 = require("./database");
58
+ // ============================================================
59
+ // Constants
60
+ // ============================================================
61
+ const SEMO_DIR = path.join(os.homedir(), ".claude", "semo");
62
+ const BOTS_DIR = path.join(SEMO_DIR, "bots");
63
+ function getCliVersion() {
64
+ try {
65
+ const pkgPath = path.join(__dirname, "..", "package.json");
66
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
67
+ return pkg.version || "unknown";
68
+ }
69
+ catch {
70
+ return "unknown";
71
+ }
72
+ }
73
+ // ============================================================
74
+ // Directory Setup
75
+ // ============================================================
76
+ /**
77
+ * ~/.claude/semo/ 디렉토리 트리 생성
78
+ */
79
+ function ensureSemoDir() {
80
+ const dirs = [SEMO_DIR, BOTS_DIR];
81
+ for (const dir of dirs) {
82
+ fs.mkdirSync(dir, { recursive: true });
83
+ }
84
+ }
85
+ // ============================================================
86
+ // Bot Mirrors
87
+ // ============================================================
88
+ /**
89
+ * DB bot_workspace_files → ~/.claude/semo/bots/{botId}/ 미러링
90
+ *
91
+ * 1. getActiveBotIds() → 봇 목록
92
+ * 2. 봇별 getBotWorkspaceFiles() → 파일 쓰기
93
+ * 3. DB에 없는 orphan 파일 정리
94
+ */
95
+ async function populateBotMirrors() {
96
+ const botIds = await (0, database_1.getActiveBotIds)();
97
+ if (botIds.length === 0)
98
+ return { bots: 0, files: 0 };
99
+ let totalFiles = 0;
100
+ for (const botId of botIds) {
101
+ const botDir = path.join(BOTS_DIR, botId);
102
+ const files = await (0, database_1.getBotWorkspaceFiles)(botId);
103
+ if (files.length === 0)
104
+ continue;
105
+ // 쓰기
106
+ const writtenPaths = new Set();
107
+ for (const file of files) {
108
+ const filePath = path.join(botDir, file.file_path);
109
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
110
+ fs.writeFileSync(filePath, file.content);
111
+ writtenPaths.add(file.file_path);
112
+ totalFiles++;
113
+ }
114
+ // Orphan 정리: DB에 없는 로컬 파일 삭제
115
+ if (fs.existsSync(botDir)) {
116
+ cleanOrphans(botDir, botDir, writtenPaths);
117
+ }
118
+ }
119
+ // 봇 디렉토리 중 DB에 없는 것 정리
120
+ if (fs.existsSync(BOTS_DIR)) {
121
+ const localBotDirs = fs.readdirSync(BOTS_DIR).filter(f => fs.statSync(path.join(BOTS_DIR, f)).isDirectory());
122
+ for (const dir of localBotDirs) {
123
+ if (!botIds.includes(dir)) {
124
+ fs.rmSync(path.join(BOTS_DIR, dir), { recursive: true, force: true });
125
+ }
126
+ }
127
+ }
128
+ return { bots: botIds.length, files: totalFiles };
129
+ }
130
+ function cleanOrphans(baseDir, currentDir, validPaths) {
131
+ if (!fs.existsSync(currentDir))
132
+ return;
133
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
134
+ for (const entry of entries) {
135
+ const fullPath = path.join(currentDir, entry.name);
136
+ const relativePath = path.relative(baseDir, fullPath);
137
+ if (entry.isDirectory()) {
138
+ cleanOrphans(baseDir, fullPath, validPaths);
139
+ // 빈 디렉토리 정리
140
+ try {
141
+ const remaining = fs.readdirSync(fullPath);
142
+ if (remaining.length === 0)
143
+ fs.rmdirSync(fullPath);
144
+ }
145
+ catch { /* ignore */ }
146
+ }
147
+ else if (!validPaths.has(relativePath)) {
148
+ fs.unlinkSync(fullPath);
149
+ }
150
+ }
151
+ }
152
+ // ============================================================
153
+ // SOUL.md / MEMORY.md / USER.md Generation
154
+ // ============================================================
155
+ /**
156
+ * 로컬 오케스트레이터 SOUL.md 생성
157
+ * (봇 SOUL.md와 동일 포맷 — bot_workspace_standard 준수)
158
+ */
159
+ async function generateSoulMd() {
160
+ const version = getCliVersion();
161
+ // 봇 로스터 생성
162
+ let botRoster = "";
163
+ try {
164
+ const pool = (0, database_1.getPool)();
165
+ const result = await pool.query(`SELECT bot_id, name, emoji, role, status
166
+ FROM semo.bot_status
167
+ WHERE status != 'retired'
168
+ ORDER BY bot_id`);
169
+ if (result.rows.length > 0) {
170
+ botRoster = "| Bot | Name | Role | Status |\n|-----|------|------|--------|\n";
171
+ for (const row of result.rows) {
172
+ botRoster += `| ${row.emoji || ""} ${row.bot_id} | ${row.name || row.bot_id} | ${row.role || "-"} | ${row.status || "-"} |\n`;
173
+ }
174
+ }
175
+ }
176
+ catch {
177
+ botRoster = "_DB 연결 실패 — 봇 정보를 가져올 수 없습니다._\n";
178
+ }
179
+ // 위임 매트릭스
180
+ let delegationMatrix = "";
181
+ try {
182
+ const delegations = await (0, database_1.getDelegations)();
183
+ if (delegations.length > 0) {
184
+ delegationMatrix = "\n## Delegation Matrix\n\n";
185
+ delegationMatrix += "| From | To | Type | Domains |\n|------|-----|------|--------|\n";
186
+ for (const d of delegations) {
187
+ delegationMatrix += `| ${d.from_bot_id} | ${d.to_bot_id} | ${d.delegation_type} | ${d.domains.join(", ")} |\n`;
188
+ }
189
+ }
190
+ }
191
+ catch { /* ignore */ }
192
+ const content = `# SEMO Local Orchestrator — SOUL
193
+
194
+ > v${version} | Generated by \`semo onboarding\`
195
+
196
+ ## Identity
197
+
198
+ - **Name**: SEMO Local Session
199
+ - **Role**: Human-AI orchestration interface for Semicolon team
200
+ - **Type**: Local Claude Code session (not an OpenClaw bot)
201
+
202
+ ## Mission
203
+
204
+ 사용자의 작업 요청을 적절한 봇 에이전트에 위임하거나 직접 처리한다.
205
+ 로컬 세션, KB, 봇 팀 간 컨텍스트 동기화를 유지한다.
206
+
207
+ ## Bot Roster
208
+
209
+ ${botRoster}
210
+ ${delegationMatrix}
211
+ ## Operating Procedures
212
+
213
+ 1. **KB-First**: 도메인 질문은 항상 \`semo kb search/get\`으로 KB 조회 후 답변
214
+ 2. **SoT Discipline**: 봇 목록/도메인 구조/워크스페이스 규칙을 하드코딩하지 않음
215
+ 3. **3-Party Sync**: 모든 변경은 DB ↔ KB ↔ 로컬 파일 영향을 고려
216
+
217
+ ## Constraints
218
+
219
+ - \`~/.claude/semo/bots/\` 내부 파일은 DB 미러이므로 직접 수정 금지 (sync 시 덮어씀)
220
+ - 봇 에이전트 호출 시 \`~/.claude/agents/\`의 정의를 사용
221
+ - 스킬 실행 시 \`~/.claude/skills/\`의 정의를 사용
222
+
223
+ ---
224
+
225
+ *Last updated: ${new Date().toISOString().split("T")[0]}*
226
+ `;
227
+ fs.writeFileSync(path.join(SEMO_DIR, "SOUL.md"), content);
228
+ }
229
+ /**
230
+ * KB 접근 가이드 인덱스
231
+ */
232
+ function generateMemoryMd() {
233
+ const content = `# SEMO Memory Index
234
+
235
+ > KB(Knowledge Base)는 Single Source of Truth입니다.
236
+ > 이 파일은 KB 접근 가이드이며, 실제 데이터는 DB에 있습니다.
237
+
238
+ ## KB 조회 명령어
239
+
240
+ | 명령어 | 설명 |
241
+ |--------|------|
242
+ | \`semo kb search "쿼리"\` | 벡터+텍스트 하이브리드 검색 |
243
+ | \`semo kb get <domain> <key> [sub_key]\` | domain+key 정확 조회 |
244
+ | \`semo kb list --domain <domain>\` | 도메인별 엔트리 목록 |
245
+ | \`semo kb upsert <domain> <key> [sub_key] --content "내용"\` | KB 항목 쓰기 |
246
+ | \`semo kb ontology --action <action>\` | 온톨로지 조회 |
247
+
248
+ ## 봇 워크스페이스 미러
249
+
250
+ \`~/.claude/semo/bots/\` 디렉토리에 각 봇의 워크스페이스 파일이 미러링됩니다.
251
+ 이 파일들은 \`semo context sync\` 시 DB에서 자동 갱신됩니다.
252
+
253
+ ---
254
+
255
+ *Auto-generated by semo onboarding*
256
+ `;
257
+ fs.writeFileSync(path.join(SEMO_DIR, "MEMORY.md"), content);
258
+ }
259
+ /**
260
+ * 사용자 프로필 플레이스홀더
261
+ */
262
+ function generateUserMd() {
263
+ const userMdPath = path.join(SEMO_DIR, "USER.md");
264
+ // 이미 존재하면 덮어쓰지 않음 (사용자가 커스텀할 수 있음)
265
+ if (fs.existsSync(userMdPath))
266
+ return;
267
+ const content = `# User Profile
268
+
269
+ > 이 파일은 사용자 프로필입니다.
270
+ > \`semo kb get semicolon team/{name}\`으로 KB에서 가져오거나 직접 수정하세요.
271
+
272
+ ## 기본 정보
273
+
274
+ | 항목 | 값 |
275
+ |------|-----|
276
+ | **이름** | _설정 필요_ |
277
+ | **역할** | _설정 필요_ |
278
+
279
+ ---
280
+
281
+ *수동 편집 가능 — semo onboarding이 덮어쓰지 않습니다*
282
+ `;
283
+ fs.writeFileSync(userMdPath, content);
284
+ }
285
+ // ============================================================
286
+ // Thin Router (CLAUDE.md)
287
+ // ============================================================
288
+ /**
289
+ * ~/.claude/CLAUDE.md를 얇은 라우터로 생성/교체
290
+ *
291
+ * @param kbFirstBlock - buildKbFirstBlock()의 결과 (index.ts에서 전달)
292
+ */
293
+ function generateThinRouter(kbFirstBlock) {
294
+ const version = getCliVersion();
295
+ const globalClaudeMd = path.join(os.homedir(), ".claude", "CLAUDE.md");
296
+ const content = `# Global Claude Code Configuration
297
+
298
+ > SEMO (Semicolon Orchestrate) v${version} installed.
299
+ > Orchestrator profile: \`~/.claude/semo/SOUL.md\`
300
+ > Bot roster & workspaces: \`~/.claude/semo/bots/\`
301
+
302
+ ## cmux Environment
303
+
304
+ 이 터미널은 **cmux** (Ghostty 기반 macOS 네이티브 터미널 멀티플렉서)에서 실행 중이다.
305
+ 사용자는 여러 프로젝트를 동시에 멀티태스킹하며, 각 워크스페이스가 별도 프로젝트에 대응한다.
306
+
307
+ ### cmux CLI 명령어
308
+
309
+ | 명령어 | 설명 |
310
+ |--------|------|
311
+ | \`cmux identify\` | 현재 워크스페이스/서피스/패인 식별 |
312
+ | \`cmux list-workspaces\` | 열린 워크스페이스 목록 |
313
+ | \`cmux tree [--workspace <ref>]\` | 워크스페이스 내 패인/서피스/탭 구조 |
314
+ | \`cmux read-screen --workspace <ref> [--surface <ref>] [--lines N]\` | 다른 탭/패인 화면 읽기 |
315
+ | \`cmux send --workspace <ref> <text>\` | 다른 탭에 텍스트 전송 |
316
+
317
+ ### 활용 가이드라인
318
+
319
+ - 다른 워크스페이스에서 실행 중인 빌드/테스트 상태를 \`cmux read-screen\`으로 확인 가능
320
+ - 에러 로그나 서버 출력을 다른 패인에서 읽을 수 있음
321
+ - \`cmux tree\`로 현재 열려 있는 전체 작업 환경을 파악할 수 있음
322
+ - 사용자가 "다른 탭에서 뭐 돌아가고 있어?" 같은 질문을 하면 cmux 명령어로 확인
323
+
324
+ ${kbFirstBlock}
325
+ `;
326
+ // 기존 파일 백업 (마이그레이션)
327
+ if (fs.existsSync(globalClaudeMd)) {
328
+ const existing = fs.readFileSync(globalClaudeMd, "utf-8");
329
+ // 이미 thin router인지 확인
330
+ if (!existing.includes("Orchestrator profile:")) {
331
+ const backupPath = globalClaudeMd + ".bak";
332
+ fs.writeFileSync(backupPath, existing);
333
+ console.log(chalk_1.default.gray(` 기존 CLAUDE.md 백업: ${backupPath}`));
334
+ }
335
+ }
336
+ fs.writeFileSync(globalClaudeMd, content);
337
+ }
338
+ // .env는 이제 ~/.claude/semo/.env에 직접 저장됨 (index.ts writeSemoEnvFile)
339
+ // setupConfigEnvLink 제거 — v4.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {