@team-semicolon/semo-cli 4.1.1 → 4.1.4

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.
@@ -50,6 +50,7 @@ const chalk_1 = __importDefault(require("chalk"));
50
50
  const ora_1 = __importDefault(require("ora"));
51
51
  const fs = __importStar(require("fs"));
52
52
  const path = __importStar(require("path"));
53
+ const child_process_1 = require("child_process");
53
54
  const database_1 = require("../database");
54
55
  function parseIdentityMd(content) {
55
56
  const nameMatch = content.match(/\*\*Name:\*\*\s*(.+)/);
@@ -343,11 +344,23 @@ function registerBotsCommands(program) {
343
344
  catch (err) {
344
345
  await client.query("ROLLBACK");
345
346
  spinner.fail(`sync 실패: ${err}`);
346
- process.exit(1);
347
- }
348
- finally {
349
347
  client.release();
350
348
  await (0, database_1.closeConnection)();
349
+ process.exit(1);
350
+ }
351
+ client.release();
352
+ await (0, database_1.closeConnection)();
353
+ // sessions sync 연동: DB 연결 반납 후 별도 프로세스로 실행
354
+ console.log(chalk_1.default.gray(" → sessions sync 실행 중..."));
355
+ try {
356
+ const semoCmd = process.argv[1];
357
+ (0, child_process_1.spawnSync)(process.execPath, [semoCmd, "sessions", "sync", "--all"], {
358
+ stdio: "inherit",
359
+ timeout: 60000,
360
+ });
361
+ }
362
+ catch {
363
+ console.log(chalk_1.default.yellow(" ⚠ sessions sync 호출 실패 (무시)"));
351
364
  }
352
365
  });
353
366
  // ── semo bots set-status ─────────────────────────────────────
@@ -1,16 +1,29 @@
1
1
  /**
2
2
  * semo sessions — 세션 추적
3
3
  *
4
- * Claude Code (SessionStart / Stop)에서 stdin으로 전달되는 JSON을 파싱해
5
- * semo.bot_sessions 테이블에 upsert합니다.
4
+ * sync: 봇의 OpenClaw 게이트웨이(HTTP API) 또는 로컬 sessions.json에서
5
+ * 세션 데이터를 읽어 semo.bot_sessions upsert합니다.
6
6
  *
7
- * Claude Code hook stdin 구조:
8
- * SessionStart: { session_id, transcript_path, cwd, hook_event_name }
9
- * Stop: { session_id, transcript_path, hook_event_name }
7
+ * push: Claude Code 훅(SessionStart / Stop)에서 stdin으로 전달되는 JSON을 파싱해
8
+ * semo.bot_sessions 테이블에 upsert합니다. (Claude Code 직접 실행 시에만 유효)
10
9
  *
11
- * 사용법 (settings.json hooks):
12
- * SessionStart semo sessions push --bot-id workclaw --event start
13
- * Stop → semo sessions push --bot-id workclaw --event stop
10
+ * OpenClaw 봇은 게이트웨이(WS/HTTP 서버)로 운영되어 Claude Code 훅이 트리거되지 않으므로
11
+ * semo sessions sync --all 주기적으로 동기화해야 합니다.
12
+ *
13
+ * 게이트웨이 설정 위치: ~/.openclaw-{botId}/openclaw.json
14
+ * gateway.port: 포트번호
15
+ * gateway.auth.token: Bearer 토큰
16
+ *
17
+ * HTTP API: POST http://127.0.0.1:{port}/tools/invoke
18
+ * { tool: "sessions_list", action: "json", args: {} }
19
+ * 응답: { ok: true, result: { content: [{ type: "text", text: "<JSON string>" }] } }
20
+ * inner JSON: { count: N, sessions: [{ key, kind, channel, displayName, updatedAt, totalTokens }] }
21
+ *
22
+ * Fallback: ~/.openclaw-{botId}/agents/main/sessions/sessions.json
23
+ * { "agent:main:slack:channel:xxx": { updatedAt: <unix ms>, ... }, ... }
14
24
  */
15
25
  import { Command } from "commander";
26
+ export declare function syncBotSessions(botIds: string[], client: any): Promise<{
27
+ total: number;
28
+ }>;
16
29
  export declare function registerSessionsCommands(program: Command): void;
@@ -2,16 +2,26 @@
2
2
  /**
3
3
  * semo sessions — 세션 추적
4
4
  *
5
- * Claude Code (SessionStart / Stop)에서 stdin으로 전달되는 JSON을 파싱해
6
- * semo.bot_sessions 테이블에 upsert합니다.
5
+ * sync: 봇의 OpenClaw 게이트웨이(HTTP API) 또는 로컬 sessions.json에서
6
+ * 세션 데이터를 읽어 semo.bot_sessions upsert합니다.
7
7
  *
8
- * Claude Code hook stdin 구조:
9
- * SessionStart: { session_id, transcript_path, cwd, hook_event_name }
10
- * Stop: { session_id, transcript_path, hook_event_name }
8
+ * push: Claude Code 훅(SessionStart / Stop)에서 stdin으로 전달되는 JSON을 파싱해
9
+ * semo.bot_sessions 테이블에 upsert합니다. (Claude Code 직접 실행 시에만 유효)
11
10
  *
12
- * 사용법 (settings.json hooks):
13
- * SessionStart semo sessions push --bot-id workclaw --event start
14
- * Stop → semo sessions push --bot-id workclaw --event stop
11
+ * OpenClaw 봇은 게이트웨이(WS/HTTP 서버)로 운영되어 Claude Code 훅이 트리거되지 않으므로
12
+ * semo sessions sync --all 주기적으로 동기화해야 합니다.
13
+ *
14
+ * 게이트웨이 설정 위치: ~/.openclaw-{botId}/openclaw.json
15
+ * gateway.port: 포트번호
16
+ * gateway.auth.token: Bearer 토큰
17
+ *
18
+ * HTTP API: POST http://127.0.0.1:{port}/tools/invoke
19
+ * { tool: "sessions_list", action: "json", args: {} }
20
+ * 응답: { ok: true, result: { content: [{ type: "text", text: "<JSON string>" }] } }
21
+ * inner JSON: { count: N, sessions: [{ key, kind, channel, displayName, updatedAt, totalTokens }] }
22
+ *
23
+ * Fallback: ~/.openclaw-{botId}/agents/main/sessions/sessions.json
24
+ * { "agent:main:slack:channel:xxx": { updatedAt: <unix ms>, ... }, ... }
15
25
  */
16
26
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
27
  if (k2 === undefined) k2 = k;
@@ -50,13 +60,94 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
50
60
  return (mod && mod.__esModule) ? mod : { "default": mod };
51
61
  };
52
62
  Object.defineProperty(exports, "__esModule", { value: true });
63
+ exports.syncBotSessions = syncBotSessions;
53
64
  exports.registerSessionsCommands = registerSessionsCommands;
54
65
  const chalk_1 = __importDefault(require("chalk"));
55
66
  const fs = __importStar(require("fs"));
56
67
  const path = __importStar(require("path"));
57
68
  const readline = __importStar(require("readline"));
69
+ const os = __importStar(require("os"));
58
70
  const child_process_1 = require("child_process");
59
71
  const database_1 = require("../database");
72
+ function readOpenClawConfig(botId) {
73
+ const configPath = path.join(os.homedir(), `.openclaw-${botId}`, "openclaw.json");
74
+ if (!fs.existsSync(configPath))
75
+ return null;
76
+ try {
77
+ return JSON.parse(fs.readFileSync(configPath, "utf-8"));
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ // HTTP API로 게이트웨이에서 세션 목록 조회
84
+ async function fetchSessionsFromGateway(botId) {
85
+ const config = readOpenClawConfig(botId);
86
+ if (!config?.gateway?.port || !config?.gateway?.auth?.token)
87
+ return null;
88
+ const { port, auth } = config.gateway;
89
+ const token = auth.token;
90
+ const url = `http://127.0.0.1:${port}/tools/invoke`;
91
+ try {
92
+ const res = await fetch(url, {
93
+ method: "POST",
94
+ headers: {
95
+ "Authorization": `Bearer ${token}`,
96
+ "Content-Type": "application/json",
97
+ },
98
+ body: JSON.stringify({ tool: "sessions_list", action: "json", args: {} }),
99
+ signal: AbortSignal.timeout(5000),
100
+ });
101
+ if (!res.ok)
102
+ return null;
103
+ const outer = await res.json();
104
+ if (!outer.ok)
105
+ return null;
106
+ // 이중 JSON 파싱: result.content[0].text가 JSON 문자열
107
+ const textContent = outer.result?.content?.find(c => c.type === "text")?.text;
108
+ if (!textContent)
109
+ return null;
110
+ const inner = JSON.parse(textContent);
111
+ return inner.sessions ?? null;
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ // Fallback: 로컬 sessions.json 파일 파싱
118
+ function readSessionsFromFile(botId) {
119
+ const sessionsPath = path.join(os.homedir(), `.openclaw-${botId}`, "agents", "main", "sessions", "sessions.json");
120
+ if (!fs.existsSync(sessionsPath))
121
+ return null;
122
+ try {
123
+ const raw = JSON.parse(fs.readFileSync(sessionsPath, "utf-8"));
124
+ return Object.entries(raw).map(([key, val]) => ({
125
+ key,
126
+ updatedAt: val.updatedAt,
127
+ }));
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ // GatewaySession → DB 컬럼 매핑
134
+ function mapSessionToDb(s) {
135
+ // kind: "group" → isolated, 나머지 → main
136
+ const kind = s.kind === "group" ? "isolated" : "main";
137
+ // chat_type: key 패턴으로 파싱 (API의 channel 필드 우선)
138
+ let chatType = s.channel ?? "direct";
139
+ if (!s.channel) {
140
+ if (s.key.includes(":slack:"))
141
+ chatType = "slack";
142
+ else if (s.key.includes(":cron:"))
143
+ chatType = "cron";
144
+ }
145
+ // label: displayName 우선, 없으면 key에서 마지막 부분
146
+ const label = s.displayName ?? s.key.split(":").slice(-2).join(":");
147
+ // last_activity: unix ms → ISO string
148
+ const lastActivity = s.updatedAt ? new Date(s.updatedAt).toISOString() : null;
149
+ return { kind, chatType, label, lastActivity, totalTokens: s.totalTokens ?? null };
150
+ }
60
151
  async function readStdin() {
61
152
  // stdin이 TTY면 hook에서 호출된 게 아님 → 빈 객체 반환
62
153
  if (process.stdin.isTTY)
@@ -120,6 +211,57 @@ async function countMessages(transcriptPath) {
120
211
  rl.on("error", () => resolve(0));
121
212
  });
122
213
  }
214
+ // ─── 외부에서 호출 가능한 sync 헬퍼 ──────────────────────────────────────────
215
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
216
+ async function syncBotSessions(botIds, client) {
217
+ let totalUpserted = 0;
218
+ for (const botId of botIds) {
219
+ let sessions = await fetchSessionsFromGateway(botId);
220
+ if (!sessions)
221
+ sessions = readSessionsFromFile(botId);
222
+ if (!sessions || sessions.length === 0)
223
+ continue;
224
+ let upserted = 0;
225
+ let latestActivity = null;
226
+ for (const s of sessions) {
227
+ try {
228
+ const { kind, chatType, label, lastActivity, totalTokens } = mapSessionToDb(s);
229
+ await client.query(`INSERT INTO semo.bot_sessions
230
+ (bot_id, session_key, label, kind, chat_type, last_activity, message_count, synced_at)
231
+ VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
232
+ ON CONFLICT (bot_id, session_key) DO UPDATE SET
233
+ label = EXCLUDED.label,
234
+ kind = EXCLUDED.kind,
235
+ chat_type = EXCLUDED.chat_type,
236
+ last_activity = COALESCE(EXCLUDED.last_activity, semo.bot_sessions.last_activity),
237
+ message_count = COALESCE(EXCLUDED.message_count, semo.bot_sessions.message_count),
238
+ synced_at = NOW()`, [botId, s.key, label, kind, chatType, lastActivity, totalTokens]);
239
+ upserted++;
240
+ if (lastActivity) {
241
+ const d = new Date(lastActivity);
242
+ if (!latestActivity || d > latestActivity)
243
+ latestActivity = d;
244
+ }
245
+ }
246
+ catch { /* 개별 세션 실패 무시 */ }
247
+ }
248
+ try {
249
+ await client.query(`UPDATE semo.bot_status
250
+ SET session_count = (SELECT COUNT(*) FROM semo.bot_sessions WHERE bot_id = $1),
251
+ last_active = CASE
252
+ WHEN $2::timestamptz IS NOT NULL
253
+ AND (last_active IS NULL OR $2::timestamptz > last_active)
254
+ THEN $2::timestamptz
255
+ ELSE last_active
256
+ END,
257
+ synced_at = NOW()
258
+ WHERE bot_id = $1`, [botId, latestActivity?.toISOString() ?? null]);
259
+ }
260
+ catch { /* bot_status 없으면 무시 */ }
261
+ totalUpserted += upserted;
262
+ }
263
+ return { total: totalUpserted };
264
+ }
123
265
  // ─── Command registration ─────────────────────────────────────────────────────
124
266
  function registerSessionsCommands(program) {
125
267
  const sessionsCmd = program
@@ -189,6 +331,13 @@ function registerSessionsCommands(program) {
189
331
  last_activity = NOW(),
190
332
  message_count = COALESCE(EXCLUDED.message_count, semo.bot_sessions.message_count),
191
333
  synced_at = NOW()`, [botId, sessionKey, label, options.kind, messageCount ?? null]);
334
+ // bot_status.session_count 갱신
335
+ await client.query(`UPDATE semo.bot_status
336
+ SET session_count = (
337
+ SELECT COUNT(*) FROM semo.bot_sessions WHERE bot_id = $1
338
+ ),
339
+ synced_at = NOW()
340
+ WHERE bot_id = $1`, [botId]);
192
341
  }
193
342
  client.release();
194
343
  console.log(chalk_1.default.green(`✔ sessions push [${event}] ${botId}/${sessionKey.slice(0, 8)}`));
@@ -202,6 +351,117 @@ function registerSessionsCommands(program) {
202
351
  await (0, database_1.closeConnection)();
203
352
  }
204
353
  });
354
+ // ── semo sessions sync ──────────────────────────────────────────────────────
355
+ sessionsCmd
356
+ .command("sync")
357
+ .description("OpenClaw 게이트웨이에서 세션 읽어 DB upsert")
358
+ .option("--bot-id <id>", "특정 봇만 동기화")
359
+ .option("--all", "semo.bot_status의 모든 봇 동기화")
360
+ .action(async (options) => {
361
+ const connected = await (0, database_1.isDbConnected)();
362
+ if (!connected) {
363
+ console.log(chalk_1.default.red("❌ DB 연결 실패"));
364
+ await (0, database_1.closeConnection)();
365
+ process.exit(1);
366
+ }
367
+ const pool = (0, database_1.getPool)();
368
+ const client = await pool.connect();
369
+ // 대상 봇 목록 결정
370
+ let botIds = [];
371
+ if (options.botId) {
372
+ botIds = [options.botId];
373
+ }
374
+ else if (options.all) {
375
+ try {
376
+ const r = await client.query("SELECT bot_id FROM semo.bot_status ORDER BY bot_id");
377
+ botIds = r.rows.map((row) => row.bot_id);
378
+ }
379
+ catch {
380
+ // bot_status 없으면 openclaw 디렉토리에서 자동 감지
381
+ const home = os.homedir();
382
+ botIds = fs.readdirSync(home)
383
+ .filter(d => d.startsWith(".openclaw-"))
384
+ .map(d => d.replace(".openclaw-", ""))
385
+ .filter(id => id.length > 0);
386
+ }
387
+ }
388
+ else {
389
+ console.log(chalk_1.default.yellow(" --bot-id <id> 또는 --all 옵션 필요"));
390
+ client.release();
391
+ await (0, database_1.closeConnection)();
392
+ process.exit(1);
393
+ }
394
+ if (botIds.length === 0) {
395
+ console.log(chalk_1.default.yellow(" 동기화할 봇 없음"));
396
+ client.release();
397
+ await (0, database_1.closeConnection)();
398
+ return;
399
+ }
400
+ console.log(chalk_1.default.cyan(`\n🔄 sessions sync — ${botIds.length}개 봇\n`));
401
+ let totalUpserted = 0;
402
+ for (const botId of botIds) {
403
+ process.stdout.write(chalk_1.default.gray(` ${botId.padEnd(14)}`));
404
+ // 1) HTTP API 시도
405
+ let sessions = await fetchSessionsFromGateway(botId);
406
+ let source = "gateway";
407
+ // 2) Fallback: 로컬 파일
408
+ if (!sessions) {
409
+ sessions = readSessionsFromFile(botId);
410
+ source = "file";
411
+ }
412
+ if (!sessions || sessions.length === 0) {
413
+ console.log(chalk_1.default.yellow("세션 없음 (게이트웨이 오프라인, 파일 없음)"));
414
+ continue;
415
+ }
416
+ // DB upsert
417
+ let upserted = 0;
418
+ let latestActivity = null;
419
+ for (const s of sessions) {
420
+ try {
421
+ const { kind, chatType, label, lastActivity, totalTokens } = mapSessionToDb(s);
422
+ await client.query(`INSERT INTO semo.bot_sessions
423
+ (bot_id, session_key, label, kind, chat_type, last_activity, message_count, synced_at)
424
+ VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
425
+ ON CONFLICT (bot_id, session_key) DO UPDATE SET
426
+ label = EXCLUDED.label,
427
+ kind = EXCLUDED.kind,
428
+ chat_type = EXCLUDED.chat_type,
429
+ last_activity = COALESCE(EXCLUDED.last_activity, semo.bot_sessions.last_activity),
430
+ message_count = COALESCE(EXCLUDED.message_count, semo.bot_sessions.message_count),
431
+ synced_at = NOW()`, [botId, s.key, label, kind, chatType, lastActivity, totalTokens]);
432
+ upserted++;
433
+ if (lastActivity) {
434
+ const d = new Date(lastActivity);
435
+ if (!latestActivity || d > latestActivity)
436
+ latestActivity = d;
437
+ }
438
+ }
439
+ catch {
440
+ // 개별 세션 실패는 무시
441
+ }
442
+ }
443
+ // bot_status session_count + last_active 갱신
444
+ try {
445
+ await client.query(`UPDATE semo.bot_status
446
+ SET session_count = (SELECT COUNT(*) FROM semo.bot_sessions WHERE bot_id = $1),
447
+ last_active = CASE
448
+ WHEN $2::timestamptz IS NOT NULL
449
+ AND (last_active IS NULL OR $2::timestamptz > last_active)
450
+ THEN $2::timestamptz
451
+ ELSE last_active
452
+ END,
453
+ synced_at = NOW()
454
+ WHERE bot_id = $1`, [botId, latestActivity?.toISOString() ?? null]);
455
+ }
456
+ catch { /* bot_status 없으면 무시 */ }
457
+ totalUpserted += upserted;
458
+ console.log(chalk_1.default.green(`✔ ${upserted}개`) +
459
+ chalk_1.default.gray(` (${source}, total ${sessions.length})`));
460
+ }
461
+ client.release();
462
+ console.log(chalk_1.default.green(`\n✅ sessions sync 완료 — 총 ${totalUpserted}건 upsert\n`));
463
+ await (0, database_1.closeConnection)();
464
+ });
205
465
  // ── semo sessions list ───────────────────────────────────────────────────────
206
466
  sessionsCmd
207
467
  .command("list")
package/dist/index.js CHANGED
@@ -676,6 +676,11 @@ async function confirmOverwrite(itemName, itemPath) {
676
676
  if (!fs.existsSync(itemPath)) {
677
677
  return true;
678
678
  }
679
+ // 비인터랙티브 환경(CI, 파이프) — 덮어쓰지 않고 기존 파일 유지
680
+ if (!process.stdin.isTTY) {
681
+ console.log(chalk_1.default.gray(` → ${itemName} 이미 존재 (비인터랙티브 모드: 건너뜀)`));
682
+ return false;
683
+ }
679
684
  const { shouldOverwrite } = await inquirer_1.default.prompt([
680
685
  {
681
686
  type: "confirm",
@@ -776,7 +781,6 @@ program
776
781
  .option("--credentials-gist <gistId>", "Private GitHub Gist에서 팀 DB 접속정보 자동 가져오기")
777
782
  .action(async (options) => {
778
783
  console.log(chalk_1.default.cyan.bold("\n🚀 SEMO 설치 시작\n"));
779
- console.log(chalk_1.default.gray("Gemini 하이브리드 전략: White Box + Black Box\n"));
780
784
  const cwd = process.cwd();
781
785
  // 0.1. 버전 비교
782
786
  await showVersionComparison(cwd);
@@ -810,6 +814,8 @@ program
810
814
  fs.mkdirSync(claudeDir, { recursive: true });
811
815
  console.log(chalk_1.default.green("\n✓ .claude/ 디렉토리 생성됨"));
812
816
  }
817
+ // 2.5. ~/.semo.env DB 접속 설정 (자동 감지 → Gist → 프롬프트) — DB 연결 전에 먼저
818
+ await setupSemoEnv(options.credentialsGist);
813
819
  // 3. Standard 설치 (semo-core + semo-skills)
814
820
  await setupStandard(cwd, options.force);
815
821
  // 4. MCP 설정
@@ -822,10 +828,8 @@ program
822
828
  if (options.gitignore !== false) {
823
829
  updateGitignore(cwd);
824
830
  }
825
- // 7. Hooks 설치 (대화 로깅)
831
+ // 7. Hooks 설치
826
832
  await setupHooks(cwd, false);
827
- // 7.5. ~/.semo.env DB 접속 설정 (자동 감지 → Gist → 프롬프트)
828
- await setupSemoEnv(options.credentialsGist);
829
833
  // 8. CLAUDE.md 생성
830
834
  await setupClaudeMd(cwd, [], options.force);
831
835
  // 9. 설치 검증
@@ -845,11 +849,11 @@ program
845
849
  console.log(chalk_1.default.yellow.bold("\n⚠️ SEMO 설치 완료 (일부 문제 발견)\n"));
846
850
  }
847
851
  console.log(chalk_1.default.cyan("설치된 구성:"));
848
- console.log(chalk_1.default.gray(" [Standard]"));
849
- console.log(chalk_1.default.gray(" semo-core (원칙, 오케스트레이터)"));
850
- console.log(chalk_1.default.gray(" semo-skills (13개 통합 스킬)"));
851
- console.log(chalk_1.default.gray(" semo-agents (14개 페르소나 Agent)"));
852
- console.log(chalk_1.default.gray(" semo-scripts (자동화 스크립트)"));
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 ( 등록)"));
853
857
  console.log(chalk_1.default.cyan("\n다음 단계:"));
854
858
  console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기 (SessionStart 훅이 자동 sync)"));
855
859
  console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
@@ -929,8 +933,6 @@ async function setupStandard(cwd, force) {
929
933
  }
930
934
  console.log(chalk_1.default.green(` ✓ agents 설치 완료 (${agents.length}개)`));
931
935
  spinner.succeed("Standard 설치 완료 (DB 기반)");
932
- // CLAUDE.md 생성
933
- await generateClaudeMd(cwd);
934
936
  }
935
937
  catch (error) {
936
938
  spinner.fail("Standard 설치 실패");
@@ -1629,54 +1631,11 @@ semo-system/
1629
1631
  async function setupHooks(cwd, isUpdate = false) {
1630
1632
  const action = isUpdate ? "업데이트" : "설치";
1631
1633
  console.log(chalk_1.default.cyan(`\n🪝 Claude Code Hooks ${action}`));
1632
- console.log(chalk_1.default.gray(" 전체 대화 로깅 시스템\n"));
1633
- const hooksDir = path.join(cwd, "semo-system", "semo-hooks");
1634
- // semo-hooks 디렉토리 확인
1635
- if (!fs.existsSync(hooksDir)) {
1636
- console.log(chalk_1.default.yellow(" ⚠ semo-hooks 디렉토리 없음 (건너뜀)"));
1637
- return;
1638
- }
1639
- // 1. npm install
1640
- console.log(chalk_1.default.gray(" → 의존성 설치 중..."));
1641
- try {
1642
- (0, child_process_1.execSync)("npm install", {
1643
- cwd: hooksDir,
1644
- stdio: ["pipe", "pipe", "pipe"],
1645
- });
1646
- }
1647
- catch {
1648
- console.log(chalk_1.default.yellow(" ⚠ npm install 실패 (건너뜀)"));
1649
- return;
1650
- }
1651
- // 2. 빌드
1652
- console.log(chalk_1.default.gray(" → 빌드 중..."));
1653
- try {
1654
- (0, child_process_1.execSync)("npm run build", {
1655
- cwd: hooksDir,
1656
- stdio: ["pipe", "pipe", "pipe"],
1657
- });
1658
- }
1659
- catch {
1660
- console.log(chalk_1.default.yellow(" ⚠ 빌드 실패 (건너뜀)"));
1661
- return;
1662
- }
1663
- // 3. settings.local.json 설정
1664
1634
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
1665
1635
  const settingsPath = path.join(homeDir, ".claude", "settings.local.json");
1666
- const hooksCmd = `node ${path.join(hooksDir, "dist", "index.js")}`;
1667
- // hooks 설정 객체
1636
+ // Core 훅: semo context sync/push — semo-hooks 유무와 무관하게 항상 등록
1668
1637
  const hooksConfig = {
1669
1638
  SessionStart: [
1670
- {
1671
- matcher: "",
1672
- hooks: [
1673
- {
1674
- type: "command",
1675
- command: `${hooksCmd} session-start`,
1676
- timeout: 10,
1677
- },
1678
- ],
1679
- },
1680
1639
  {
1681
1640
  matcher: "",
1682
1641
  hooks: [
@@ -1688,29 +1647,7 @@ async function setupHooks(cwd, isUpdate = false) {
1688
1647
  ],
1689
1648
  },
1690
1649
  ],
1691
- UserPromptSubmit: [
1692
- {
1693
- matcher: "",
1694
- hooks: [
1695
- {
1696
- type: "command",
1697
- command: `${hooksCmd} user-prompt`,
1698
- timeout: 5,
1699
- },
1700
- ],
1701
- },
1702
- ],
1703
1650
  Stop: [
1704
- {
1705
- matcher: "",
1706
- hooks: [
1707
- {
1708
- type: "command",
1709
- command: `${hooksCmd} stop`,
1710
- timeout: 10,
1711
- },
1712
- ],
1713
- },
1714
1651
  {
1715
1652
  matcher: "",
1716
1653
  hooks: [
@@ -1722,19 +1659,37 @@ async function setupHooks(cwd, isUpdate = false) {
1722
1659
  ],
1723
1660
  },
1724
1661
  ],
1725
- SessionEnd: [
1726
- {
1727
- matcher: "",
1728
- hooks: [
1729
- {
1730
- type: "command",
1731
- command: `${hooksCmd} session-end`,
1732
- timeout: 10,
1733
- },
1734
- ],
1735
- },
1736
- ],
1737
1662
  };
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
+ }
1738
1693
  // 기존 설정 로드 또는 새로 생성
1739
1694
  let existingSettings = {};
1740
1695
  const claudeConfigDir = path.join(homeDir, ".claude");
@@ -1935,133 +1890,132 @@ async function setupClaudeMd(cwd, _extensions, force) {
1935
1890
  return;
1936
1891
  }
1937
1892
  }
1938
- const orchestratorRefSection = `**반드시 읽어야 할 파일**: \`semo-system/semo-core/agents/orchestrator/orchestrator.md\`
1939
-
1940
- 파일에서 라우팅 테이블, 의도 분류, 메시지 포맷을 확인하세요.`;
1941
- const claudeMdContent = `# SEMO Project Configuration
1893
+ const projectName = path.basename(cwd);
1894
+ const installDate = new Date().toISOString().split("T")[0];
1895
+ const claudeMdContent = `# ${projectName} Claude Configuration
1942
1896
 
1943
- > SEMO (Semicolon Orchestrate) - AI Agent Orchestration Framework v${VERSION}
1897
+ > SEMO v${VERSION} 설치됨 (${installDate})
1944
1898
 
1945
1899
  ---
1946
1900
 
1947
- ## 🔴 MANDATORY: Memory Context (항시 참조)
1948
-
1949
- > **⚠️ 세션 시작 시 반드시 \`.claude/memory/\` 폴더의 파일들을 먼저 읽으세요. 예외 없음.**
1901
+ ## SEMO란?
1950
1902
 
1951
- ### 필수 참조 파일
1903
+ **SEMO (Semicolon Orchestrate)** 는 OpenClaw 봇팀과 로컬 Claude Code 세션이
1904
+ **팀 Core DB를 단일 진실 공급원(Single Source of Truth)으로 공유**하는 컨텍스트 동기화 시스템이다.
1952
1905
 
1953
1906
  \`\`\`
1954
- .claude/memory/
1955
- ├── context.md # 프로젝트 상태, 기술 스택, 진행 중 작업
1956
- ├── decisions.md # 아키텍처 결정 기록 (ADR)
1957
- ├── projects.md # GitHub Projects 설정
1958
- └── rules/ # 프로젝트별 커스텀 규칙
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
1959
1914
  \`\`\`
1960
1915
 
1961
- **이 파일들은 세션의 컨텍스트를 유지하는 장기 기억입니다. 세션마다 반드시 읽고 시작하세요.**
1916
+ **이 CLAUDE.md가 설치된 프로젝트는 OpenClaw 봇팀의 컨텍스트를 실시간으로 공유받는다.**
1962
1917
 
1963
1918
  ---
1964
1919
 
1965
- ## 🔴 MANDATORY: Orchestrator-First Execution
1920
+ ## 자동 동기화
1966
1921
 
1967
- > **⚠️ 규칙은 모든 사용자 요청에 적용됩니다. 예외 없음.**
1922
+ 세션 시작/종료 Core DB와 자동 동기화됩니다.
1968
1923
 
1969
- ### 실행 흐름 (필수)
1924
+ | 시점 | 동작 |
1925
+ |------|------|
1926
+ | 세션 시작 | \`semo context sync\` → \`.claude/memory/\` 최신화 |
1927
+ | 세션 종료 | \`semo context push\` → \`decisions.md\` 변경분 DB 저장 |
1928
+
1929
+ ---
1930
+
1931
+ ## Memory Context
1932
+
1933
+ \`.claude/memory/\` 파일들은 **팀 Core DB (\`semo\` 스키마)에서 자동으로 채워집니다**.
1934
+ 직접 편집하지 마세요 — 세션 시작 시 덮어씌워집니다.
1935
+
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 → 로컬 (읽기 전용) |
1945
+
1946
+ **decisions.md 만 편집 가능합니다.** 아키텍처 결정(ADR)을 여기에 기록하세요.
1947
+
1948
+ ---
1949
+
1950
+ ## 설치된 구성
1970
1951
 
1971
1952
  \`\`\`
1972
- 1. 사용자 요청 수신
1973
- 2. Orchestrator가 의도 분석 후 적절한 Agent/Skill 라우팅
1974
- 3. Agent/Skill이 작업 수행
1975
- 4. 실행 결과 반환
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/)
1976
1960
  \`\`\`
1977
1961
 
1978
- ### Orchestrator 참조
1962
+ ---
1979
1963
 
1980
- ${orchestratorRefSection}
1964
+ ## 프로젝트 규칙 (팀이 채워야 함)
1981
1965
 
1982
- ---
1966
+ > 아래 섹션은 이 프로젝트 고유의 규칙을 기록하세요.
1967
+ > 팀 공통 규칙은 \`memory/process.md\`에 있습니다.
1983
1968
 
1984
- ## 🔴 NON-NEGOTIABLE RULES
1969
+ ### 기술 스택
1985
1970
 
1986
- ### 1. Orchestrator-First Policy
1971
+ <!-- 예: Next.js 14, PostgreSQL, TypeScript strict mode -->
1987
1972
 
1988
- > **모든 요청은 반드시 Orchestrator를 통해 라우팅됩니다. 직접 처리 금지.**
1973
+ ### 브랜치 전략
1989
1974
 
1990
- **직접 처리 금지 항목**:
1991
- - 코드 작성/수정 → \`implementation-master\` 또는 \`coder\` 스킬
1992
- - Git 커밋/푸시 → \`git-workflow\` 스킬
1993
- - 품질 검증 → \`quality-master\` 또는 \`verify\` 스킬
1994
- - 명세 작성 → \`spec-master\`
1995
- - 일반 작업 → Orchestrator 분석 후 라우팅
1975
+ <!-- 예: main(prod) / dev(staging) / feat/* -->
1996
1976
 
1997
- ### 2. Pre-Commit Quality Gate
1977
+ ### 코딩 컨벤션
1998
1978
 
1999
- > **코드 변경이 포함된 커밋 반드시 Quality Gate를 통과해야 합니다.**
1979
+ <!-- 예: ESLint airbnb, 함수형 컴포넌트 필수, any 금지 -->
2000
1980
 
2001
- \`\`\`bash
2002
- # 필수 검증 순서
2003
- npm run lint # 1. ESLint 검사
2004
- npx tsc --noEmit # 2. TypeScript 타입 체크
2005
- npm run build # 3. 빌드 검증 (Next.js/TypeScript 프로젝트)
2006
- \`\`\`
1981
+ ### 아키텍처 특이사항
2007
1982
 
2008
- **차단 항목**:
2009
- - \`--no-verify\` 플래그 사용 금지
2010
- - Quality Gate 우회 시도 거부
2011
- - "그냥 커밋해줘", "빌드 생략해줘" 등 거부
1983
+ <!-- 예: DB 직접 접근 금지 — 반드시 API route 통해야 함 -->
2012
1984
 
2013
1985
  ---
2014
1986
 
2015
- ## 설치된 구성
2016
-
2017
- ### Standard (필수)
2018
- - **semo-core**: 원칙, 오케스트레이터, 공통 커맨드
2019
- - **semo-skills**: 13개 통합 스킬
2020
- - 행동: coder, tester, planner, deployer, writer
2021
- - 운영: memory, notify-slack, feedback, version-updater, semo-help, semo-architecture-checker, circuit-breaker, list-bugs
1987
+ ## Quality Gate
2022
1988
 
2023
- ## 구조
1989
+ 코드 변경 커밋 전 필수:
2024
1990
 
2025
- \`\`\`
2026
- .claude/
2027
- ├── settings.json # MCP 서버 설정 (Black Box)
2028
- ├── memory/ # Context Mesh (장기 기억)
2029
- │ ├── context.md # 프로젝트 상태
2030
- │ ├── decisions.md # 아키텍처 결정
2031
- │ └── rules/ # 프로젝트별 규칙
2032
- ├── agents → semo-system/semo-core/agents
2033
- ├── skills → semo-system/semo-skills
2034
- └── commands/SEMO → semo-system/semo-core/commands/SEMO
2035
-
2036
- semo-system/ # White Box (읽기 전용)
2037
- ├── semo-core/ # Layer 0: 원칙, 오케스트레이션
2038
- └── semo-skills/ # Layer 1: 통합 스킬
1991
+ \`\`\`bash
1992
+ npm run lint # ESLint
1993
+ npx tsc --noEmit # TypeScript
1994
+ npm run build # 빌드 검증
2039
1995
  \`\`\`
2040
1996
 
2041
- ## 사용 가능한 커맨드
1997
+ \`--no-verify\` 사용 금지.
1998
+
1999
+ ---
2000
+
2001
+ ## 슬래시 커맨드
2042
2002
 
2043
2003
  | 커맨드 | 설명 |
2044
2004
  |--------|------|
2045
2005
  | \`/SEMO:help\` | 도움말 |
2046
2006
  | \`/SEMO:feedback\` | 피드백 제출 |
2047
- | \`/SEMO:update\` | SEMO 업데이트 |
2048
- | \`/SEMO:onboarding\` | 온보딩 가이드 |
2049
- | \`/SEMO:dry-run {프롬프트}\` | 명령 검증 (라우팅 시뮬레이션) |
2050
-
2051
- ## Context Mesh 사용
2052
-
2053
- SEMO는 \`.claude/memory/\`를 통해 세션 간 컨텍스트를 유지합니다:
2007
+ | \`/SEMO:health\` | 환경 헬스체크 |
2054
2008
 
2055
- - **context.md**: 프로젝트 상태, 진행 중인 작업
2056
- - **decisions.md**: 아키텍처 결정 기록 (ADR)
2057
- - **rules/**: 프로젝트별 커스텀 규칙
2058
-
2059
- memory 스킬이 자동으로 이 파일들을 관리합니다.
2009
+ ---
2060
2010
 
2061
- ## References
2011
+ ## 복구 명령어
2062
2012
 
2063
- - [SEMO Principles](semo-system/semo-core/principles/PRINCIPLES.md)
2064
- - [SEMO Skills](semo-system/semo-skills/)
2013
+ \`\`\`bash
2014
+ semo doctor # 환경 진단 (DB 연결, 설치 상태)
2015
+ semo config db # DB URL 재설정
2016
+ semo context sync # memory/ 수동 최신화
2017
+ semo bots status # 봇 상태 조회
2018
+ \`\`\`
2065
2019
  `;
2066
2020
  fs.writeFileSync(claudeMdPath, claudeMdContent);
2067
2021
  console.log(chalk_1.default.green("✓ .claude/CLAUDE.md 생성됨"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "4.1.1",
3
+ "version": "4.1.4",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {