@team-semicolon/semo-cli 4.1.0 → 4.1.3
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/commands/bots.js +5 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +273 -0
- package/dist/index.js +129 -173
- package/package.json +1 -1
package/dist/commands/bots.js
CHANGED
|
@@ -330,6 +330,11 @@ function registerBotsCommands(program) {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
await client.query("COMMIT");
|
|
333
|
+
// session_count를 bot_sessions 실제 집계로 갱신
|
|
334
|
+
await client.query(`UPDATE semo.bot_status bs
|
|
335
|
+
SET session_count = (
|
|
336
|
+
SELECT COUNT(*) FROM semo.bot_sessions WHERE bot_id = bs.bot_id
|
|
337
|
+
)`);
|
|
333
338
|
spinner.succeed(`bots sync 완료: ${upserted}개 봇 업서트`);
|
|
334
339
|
if (errors.length > 0) {
|
|
335
340
|
errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semo sessions — 세션 추적
|
|
3
|
+
*
|
|
4
|
+
* Claude Code 훅(SessionStart / Stop)에서 stdin으로 전달되는 JSON을 파싱해
|
|
5
|
+
* semo.bot_sessions 테이블에 upsert합니다.
|
|
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 }
|
|
10
|
+
*
|
|
11
|
+
* 사용법 (settings.json hooks):
|
|
12
|
+
* SessionStart → semo sessions push --bot-id workclaw --event start
|
|
13
|
+
* Stop → semo sessions push --bot-id workclaw --event stop
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
export declare function registerSessionsCommands(program: Command): void;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* semo sessions — 세션 추적
|
|
4
|
+
*
|
|
5
|
+
* Claude Code 훅(SessionStart / Stop)에서 stdin으로 전달되는 JSON을 파싱해
|
|
6
|
+
* semo.bot_sessions 테이블에 upsert합니다.
|
|
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 }
|
|
11
|
+
*
|
|
12
|
+
* 사용법 (settings.json hooks):
|
|
13
|
+
* SessionStart → semo sessions push --bot-id workclaw --event start
|
|
14
|
+
* Stop → semo sessions push --bot-id workclaw --event stop
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.registerSessionsCommands = registerSessionsCommands;
|
|
54
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
const readline = __importStar(require("readline"));
|
|
58
|
+
const child_process_1 = require("child_process");
|
|
59
|
+
const database_1 = require("../database");
|
|
60
|
+
async function readStdin() {
|
|
61
|
+
// stdin이 TTY면 hook에서 호출된 게 아님 → 빈 객체 반환
|
|
62
|
+
if (process.stdin.isTTY)
|
|
63
|
+
return {};
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
let raw = "";
|
|
66
|
+
process.stdin.setEncoding("utf-8");
|
|
67
|
+
process.stdin.on("data", (chunk) => (raw += chunk));
|
|
68
|
+
process.stdin.on("end", () => {
|
|
69
|
+
try {
|
|
70
|
+
resolve(JSON.parse(raw));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
resolve({});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// 500ms 타임아웃 — stdin이 오지 않으면 그냥 진행
|
|
77
|
+
setTimeout(() => resolve({}), 500);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// ─── 현재 git 브랜치 (label용) ───────────────────────────────────────────────
|
|
81
|
+
function getGitBranch(cwd) {
|
|
82
|
+
try {
|
|
83
|
+
const dir = cwd || process.cwd();
|
|
84
|
+
return (0, child_process_1.execSync)("git rev-parse --abbrev-ref HEAD", {
|
|
85
|
+
cwd: dir,
|
|
86
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
87
|
+
timeout: 2000,
|
|
88
|
+
})
|
|
89
|
+
.toString()
|
|
90
|
+
.trim();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ─── transcript.jsonl 메시지 수 카운트 ───────────────────────────────────────
|
|
97
|
+
async function countMessages(transcriptPath) {
|
|
98
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
99
|
+
return 0;
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
let count = 0;
|
|
102
|
+
const rl = readline.createInterface({
|
|
103
|
+
input: fs.createReadStream(transcriptPath),
|
|
104
|
+
crlfDelay: Infinity,
|
|
105
|
+
});
|
|
106
|
+
rl.on("line", (line) => {
|
|
107
|
+
if (!line.trim())
|
|
108
|
+
return;
|
|
109
|
+
try {
|
|
110
|
+
const obj = JSON.parse(line);
|
|
111
|
+
// role이 있는 메시지(user/assistant)만 카운트
|
|
112
|
+
if (obj.role === "user" || obj.role === "assistant")
|
|
113
|
+
count++;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// invalid line skip
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
rl.on("close", () => resolve(count));
|
|
120
|
+
rl.on("error", () => resolve(0));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// ─── Command registration ─────────────────────────────────────────────────────
|
|
124
|
+
function registerSessionsCommands(program) {
|
|
125
|
+
const sessionsCmd = program
|
|
126
|
+
.command("sessions")
|
|
127
|
+
.description("세션 추적 (Claude Code 훅 연동)");
|
|
128
|
+
// ── semo sessions push ───────────────────────────────────────────────────────
|
|
129
|
+
sessionsCmd
|
|
130
|
+
.command("push")
|
|
131
|
+
.description("현재 세션을 semo.bot_sessions에 기록 (훅에서 호출)")
|
|
132
|
+
.requiredOption("--bot-id <id>", "봇 ID (e.g. workclaw)")
|
|
133
|
+
.option("--event <type>", "이벤트 종류 (start|stop|heartbeat)", "heartbeat")
|
|
134
|
+
.option("--label <text>", "세션 라벨 (미지정 시 git 브랜치 자동 감지)")
|
|
135
|
+
.option("--kind <kind>", "세션 종류 (main|isolated)", "main")
|
|
136
|
+
.action(async (options) => {
|
|
137
|
+
const botId = options.botId;
|
|
138
|
+
const event = options.event;
|
|
139
|
+
// stdin에서 Claude Code hook JSON 읽기
|
|
140
|
+
const hook = await readStdin();
|
|
141
|
+
const sessionKey = hook.session_id ||
|
|
142
|
+
process.env.CLAUDE_SESSION_ID ||
|
|
143
|
+
`${botId}-${Date.now()}`;
|
|
144
|
+
const branch = getGitBranch(hook.cwd);
|
|
145
|
+
const label = options.label ||
|
|
146
|
+
branch ||
|
|
147
|
+
path.basename(hook.cwd || process.cwd());
|
|
148
|
+
const messageCount = event === "stop" && hook.transcript_path
|
|
149
|
+
? await countMessages(hook.transcript_path)
|
|
150
|
+
: undefined;
|
|
151
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
152
|
+
if (!connected) {
|
|
153
|
+
// 훅에서 호출 시 조용히 실패 (봇 세션에 영향 주지 않도록)
|
|
154
|
+
await (0, database_1.closeConnection)();
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const pool = (0, database_1.getPool)();
|
|
159
|
+
const client = await pool.connect();
|
|
160
|
+
if (event === "start") {
|
|
161
|
+
await client.query(`INSERT INTO semo.bot_sessions
|
|
162
|
+
(bot_id, session_key, label, kind, chat_type, last_activity, message_count, synced_at)
|
|
163
|
+
VALUES ($1, $2, $3, $4, 'claude-code', NOW(), 0, NOW())
|
|
164
|
+
ON CONFLICT (bot_id, session_key) DO UPDATE SET
|
|
165
|
+
label = EXCLUDED.label,
|
|
166
|
+
last_activity = NOW(),
|
|
167
|
+
synced_at = NOW()`, [botId, sessionKey, label, options.kind]);
|
|
168
|
+
// bot_status.session_count 갱신
|
|
169
|
+
await client.query(`UPDATE semo.bot_status
|
|
170
|
+
SET session_count = (
|
|
171
|
+
SELECT COUNT(*) FROM semo.bot_sessions WHERE bot_id = $1
|
|
172
|
+
),
|
|
173
|
+
synced_at = NOW()
|
|
174
|
+
WHERE bot_id = $1`, [botId]);
|
|
175
|
+
}
|
|
176
|
+
else if (event === "stop") {
|
|
177
|
+
await client.query(`UPDATE semo.bot_sessions
|
|
178
|
+
SET last_activity = NOW(),
|
|
179
|
+
message_count = COALESCE($1, message_count),
|
|
180
|
+
synced_at = NOW()
|
|
181
|
+
WHERE bot_id = $2 AND session_key = $3`, [messageCount ?? null, botId, sessionKey]);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// heartbeat — 마지막 활동 시간 + 메시지 수 갱신
|
|
185
|
+
await client.query(`INSERT INTO semo.bot_sessions
|
|
186
|
+
(bot_id, session_key, label, kind, chat_type, last_activity, message_count, synced_at)
|
|
187
|
+
VALUES ($1, $2, $3, $4, 'claude-code', NOW(), COALESCE($5, 0), NOW())
|
|
188
|
+
ON CONFLICT (bot_id, session_key) DO UPDATE SET
|
|
189
|
+
last_activity = NOW(),
|
|
190
|
+
message_count = COALESCE(EXCLUDED.message_count, semo.bot_sessions.message_count),
|
|
191
|
+
synced_at = NOW()`, [botId, sessionKey, label, options.kind, messageCount ?? null]);
|
|
192
|
+
// bot_status.session_count 갱신
|
|
193
|
+
await client.query(`UPDATE semo.bot_status
|
|
194
|
+
SET session_count = (
|
|
195
|
+
SELECT COUNT(*) FROM semo.bot_sessions WHERE bot_id = $1
|
|
196
|
+
),
|
|
197
|
+
synced_at = NOW()
|
|
198
|
+
WHERE bot_id = $1`, [botId]);
|
|
199
|
+
}
|
|
200
|
+
client.release();
|
|
201
|
+
console.log(chalk_1.default.green(`✔ sessions push [${event}] ${botId}/${sessionKey.slice(0, 8)}`));
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
// 훅에서 호출 시 조용히 실패
|
|
205
|
+
console.error(chalk_1.default.red(`sessions push 실패: ${err}`));
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
await (0, database_1.closeConnection)();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
// ── semo sessions list ───────────────────────────────────────────────────────
|
|
213
|
+
sessionsCmd
|
|
214
|
+
.command("list")
|
|
215
|
+
.description("bot_sessions 테이블 조회")
|
|
216
|
+
.option("--bot-id <id>", "특정 봇만")
|
|
217
|
+
.option("--limit <n>", "최대 조회 수", "20")
|
|
218
|
+
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
219
|
+
.action(async (options) => {
|
|
220
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
221
|
+
if (!connected) {
|
|
222
|
+
console.log(chalk_1.default.red("❌ DB 연결 실패"));
|
|
223
|
+
await (0, database_1.closeConnection)();
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const pool = (0, database_1.getPool)();
|
|
228
|
+
const client = await pool.connect();
|
|
229
|
+
const params = [];
|
|
230
|
+
let where = "";
|
|
231
|
+
if (options.botId) {
|
|
232
|
+
where = "WHERE bot_id = $1";
|
|
233
|
+
params.push(options.botId);
|
|
234
|
+
}
|
|
235
|
+
params.push(parseInt(options.limit));
|
|
236
|
+
const limitIdx = params.length;
|
|
237
|
+
const result = await client.query(`SELECT bot_id, session_key, label, kind, chat_type,
|
|
238
|
+
last_activity::text, message_count
|
|
239
|
+
FROM semo.bot_sessions
|
|
240
|
+
${where}
|
|
241
|
+
ORDER BY last_activity DESC NULLS LAST
|
|
242
|
+
LIMIT $${limitIdx}`, params);
|
|
243
|
+
client.release();
|
|
244
|
+
if (options.format === "json") {
|
|
245
|
+
console.log(JSON.stringify(result.rows, null, 2));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
console.log(chalk_1.default.cyan.bold("\n📋 세션 목록\n"));
|
|
249
|
+
if (result.rows.length === 0) {
|
|
250
|
+
console.log(chalk_1.default.yellow(" 세션 없음"));
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
for (const s of result.rows) {
|
|
254
|
+
const ts = s.last_activity
|
|
255
|
+
? new Date(s.last_activity).toLocaleString("ko-KR")
|
|
256
|
+
: "-";
|
|
257
|
+
console.log(chalk_1.default.cyan(` ${s.bot_id.padEnd(14)}`) +
|
|
258
|
+
chalk_1.default.white(`${(s.label || s.session_key).padEnd(30)}`) +
|
|
259
|
+
chalk_1.default.gray(`${ts} ${s.message_count}msg`));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
console.log();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
console.log(chalk_1.default.red(`❌ 조회 실패: ${err}`));
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
await (0, database_1.closeConnection)();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -60,6 +60,7 @@ const database_1 = require("./database");
|
|
|
60
60
|
const context_1 = require("./commands/context");
|
|
61
61
|
const bots_1 = require("./commands/bots");
|
|
62
62
|
const get_1 = require("./commands/get");
|
|
63
|
+
const sessions_1 = require("./commands/sessions");
|
|
63
64
|
const PACKAGE_NAME = "@team-semicolon/semo-cli";
|
|
64
65
|
// package.json에서 버전 동적 로드
|
|
65
66
|
function getCliVersion() {
|
|
@@ -675,6 +676,11 @@ async function confirmOverwrite(itemName, itemPath) {
|
|
|
675
676
|
if (!fs.existsSync(itemPath)) {
|
|
676
677
|
return true;
|
|
677
678
|
}
|
|
679
|
+
// 비인터랙티브 환경(CI, 파이프) — 덮어쓰지 않고 기존 파일 유지
|
|
680
|
+
if (!process.stdin.isTTY) {
|
|
681
|
+
console.log(chalk_1.default.gray(` → ${itemName} 이미 존재 (비인터랙티브 모드: 건너뜀)`));
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
678
684
|
const { shouldOverwrite } = await inquirer_1.default.prompt([
|
|
679
685
|
{
|
|
680
686
|
type: "confirm",
|
|
@@ -775,7 +781,6 @@ program
|
|
|
775
781
|
.option("--credentials-gist <gistId>", "Private GitHub Gist에서 팀 DB 접속정보 자동 가져오기")
|
|
776
782
|
.action(async (options) => {
|
|
777
783
|
console.log(chalk_1.default.cyan.bold("\n🚀 SEMO 설치 시작\n"));
|
|
778
|
-
console.log(chalk_1.default.gray("Gemini 하이브리드 전략: White Box + Black Box\n"));
|
|
779
784
|
const cwd = process.cwd();
|
|
780
785
|
// 0.1. 버전 비교
|
|
781
786
|
await showVersionComparison(cwd);
|
|
@@ -809,6 +814,8 @@ program
|
|
|
809
814
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
810
815
|
console.log(chalk_1.default.green("\n✓ .claude/ 디렉토리 생성됨"));
|
|
811
816
|
}
|
|
817
|
+
// 2.5. ~/.semo.env DB 접속 설정 (자동 감지 → Gist → 프롬프트) — DB 연결 전에 먼저
|
|
818
|
+
await setupSemoEnv(options.credentialsGist);
|
|
812
819
|
// 3. Standard 설치 (semo-core + semo-skills)
|
|
813
820
|
await setupStandard(cwd, options.force);
|
|
814
821
|
// 4. MCP 설정
|
|
@@ -821,10 +828,8 @@ program
|
|
|
821
828
|
if (options.gitignore !== false) {
|
|
822
829
|
updateGitignore(cwd);
|
|
823
830
|
}
|
|
824
|
-
// 7. Hooks 설치
|
|
831
|
+
// 7. Hooks 설치
|
|
825
832
|
await setupHooks(cwd, false);
|
|
826
|
-
// 7.5. ~/.semo.env DB 접속 설정 (자동 감지 → Gist → 프롬프트)
|
|
827
|
-
await setupSemoEnv(options.credentialsGist);
|
|
828
833
|
// 8. CLAUDE.md 생성
|
|
829
834
|
await setupClaudeMd(cwd, [], options.force);
|
|
830
835
|
// 9. 설치 검증
|
|
@@ -844,11 +849,11 @@ program
|
|
|
844
849
|
console.log(chalk_1.default.yellow.bold("\n⚠️ SEMO 설치 완료 (일부 문제 발견)\n"));
|
|
845
850
|
}
|
|
846
851
|
console.log(chalk_1.default.cyan("설치된 구성:"));
|
|
847
|
-
console.log(chalk_1.default.gray("
|
|
848
|
-
console.log(chalk_1.default.gray("
|
|
849
|
-
console.log(chalk_1.default.gray("
|
|
850
|
-
console.log(chalk_1.default.gray("
|
|
851
|
-
console.log(chalk_1.default.gray("
|
|
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 (훅 등록)"));
|
|
852
857
|
console.log(chalk_1.default.cyan("\n다음 단계:"));
|
|
853
858
|
console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기 (SessionStart 훅이 자동 sync)"));
|
|
854
859
|
console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
|
|
@@ -928,8 +933,6 @@ async function setupStandard(cwd, force) {
|
|
|
928
933
|
}
|
|
929
934
|
console.log(chalk_1.default.green(` ✓ agents 설치 완료 (${agents.length}개)`));
|
|
930
935
|
spinner.succeed("Standard 설치 완료 (DB 기반)");
|
|
931
|
-
// CLAUDE.md 생성
|
|
932
|
-
await generateClaudeMd(cwd);
|
|
933
936
|
}
|
|
934
937
|
catch (error) {
|
|
935
938
|
spinner.fail("Standard 설치 실패");
|
|
@@ -1628,54 +1631,11 @@ semo-system/
|
|
|
1628
1631
|
async function setupHooks(cwd, isUpdate = false) {
|
|
1629
1632
|
const action = isUpdate ? "업데이트" : "설치";
|
|
1630
1633
|
console.log(chalk_1.default.cyan(`\n🪝 Claude Code Hooks ${action}`));
|
|
1631
|
-
console.log(chalk_1.default.gray(" 전체 대화 로깅 시스템\n"));
|
|
1632
|
-
const hooksDir = path.join(cwd, "semo-system", "semo-hooks");
|
|
1633
|
-
// semo-hooks 디렉토리 확인
|
|
1634
|
-
if (!fs.existsSync(hooksDir)) {
|
|
1635
|
-
console.log(chalk_1.default.yellow(" ⚠ semo-hooks 디렉토리 없음 (건너뜀)"));
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
// 1. npm install
|
|
1639
|
-
console.log(chalk_1.default.gray(" → 의존성 설치 중..."));
|
|
1640
|
-
try {
|
|
1641
|
-
(0, child_process_1.execSync)("npm install", {
|
|
1642
|
-
cwd: hooksDir,
|
|
1643
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1644
|
-
});
|
|
1645
|
-
}
|
|
1646
|
-
catch {
|
|
1647
|
-
console.log(chalk_1.default.yellow(" ⚠ npm install 실패 (건너뜀)"));
|
|
1648
|
-
return;
|
|
1649
|
-
}
|
|
1650
|
-
// 2. 빌드
|
|
1651
|
-
console.log(chalk_1.default.gray(" → 빌드 중..."));
|
|
1652
|
-
try {
|
|
1653
|
-
(0, child_process_1.execSync)("npm run build", {
|
|
1654
|
-
cwd: hooksDir,
|
|
1655
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1656
|
-
});
|
|
1657
|
-
}
|
|
1658
|
-
catch {
|
|
1659
|
-
console.log(chalk_1.default.yellow(" ⚠ 빌드 실패 (건너뜀)"));
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
// 3. settings.local.json 설정
|
|
1663
1634
|
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
1664
1635
|
const settingsPath = path.join(homeDir, ".claude", "settings.local.json");
|
|
1665
|
-
|
|
1666
|
-
// hooks 설정 객체
|
|
1636
|
+
// Core 훅: semo context sync/push — semo-hooks 유무와 무관하게 항상 등록
|
|
1667
1637
|
const hooksConfig = {
|
|
1668
1638
|
SessionStart: [
|
|
1669
|
-
{
|
|
1670
|
-
matcher: "",
|
|
1671
|
-
hooks: [
|
|
1672
|
-
{
|
|
1673
|
-
type: "command",
|
|
1674
|
-
command: `${hooksCmd} session-start`,
|
|
1675
|
-
timeout: 10,
|
|
1676
|
-
},
|
|
1677
|
-
],
|
|
1678
|
-
},
|
|
1679
1639
|
{
|
|
1680
1640
|
matcher: "",
|
|
1681
1641
|
hooks: [
|
|
@@ -1687,29 +1647,7 @@ async function setupHooks(cwd, isUpdate = false) {
|
|
|
1687
1647
|
],
|
|
1688
1648
|
},
|
|
1689
1649
|
],
|
|
1690
|
-
UserPromptSubmit: [
|
|
1691
|
-
{
|
|
1692
|
-
matcher: "",
|
|
1693
|
-
hooks: [
|
|
1694
|
-
{
|
|
1695
|
-
type: "command",
|
|
1696
|
-
command: `${hooksCmd} user-prompt`,
|
|
1697
|
-
timeout: 5,
|
|
1698
|
-
},
|
|
1699
|
-
],
|
|
1700
|
-
},
|
|
1701
|
-
],
|
|
1702
1650
|
Stop: [
|
|
1703
|
-
{
|
|
1704
|
-
matcher: "",
|
|
1705
|
-
hooks: [
|
|
1706
|
-
{
|
|
1707
|
-
type: "command",
|
|
1708
|
-
command: `${hooksCmd} stop`,
|
|
1709
|
-
timeout: 10,
|
|
1710
|
-
},
|
|
1711
|
-
],
|
|
1712
|
-
},
|
|
1713
1651
|
{
|
|
1714
1652
|
matcher: "",
|
|
1715
1653
|
hooks: [
|
|
@@ -1721,19 +1659,37 @@ async function setupHooks(cwd, isUpdate = false) {
|
|
|
1721
1659
|
],
|
|
1722
1660
|
},
|
|
1723
1661
|
],
|
|
1724
|
-
SessionEnd: [
|
|
1725
|
-
{
|
|
1726
|
-
matcher: "",
|
|
1727
|
-
hooks: [
|
|
1728
|
-
{
|
|
1729
|
-
type: "command",
|
|
1730
|
-
command: `${hooksCmd} session-end`,
|
|
1731
|
-
timeout: 10,
|
|
1732
|
-
},
|
|
1733
|
-
],
|
|
1734
|
-
},
|
|
1735
|
-
],
|
|
1736
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
|
+
}
|
|
1737
1693
|
// 기존 설정 로드 또는 새로 생성
|
|
1738
1694
|
let existingSettings = {};
|
|
1739
1695
|
const claudeConfigDir = path.join(homeDir, ".claude");
|
|
@@ -1934,133 +1890,132 @@ async function setupClaudeMd(cwd, _extensions, force) {
|
|
|
1934
1890
|
return;
|
|
1935
1891
|
}
|
|
1936
1892
|
}
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
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
|
|
1941
1896
|
|
|
1942
|
-
> SEMO
|
|
1897
|
+
> SEMO v${VERSION} 설치됨 (${installDate})
|
|
1943
1898
|
|
|
1944
1899
|
---
|
|
1945
1900
|
|
|
1946
|
-
##
|
|
1947
|
-
|
|
1948
|
-
> **⚠️ 세션 시작 시 반드시 \`.claude/memory/\` 폴더의 파일들을 먼저 읽으세요. 예외 없음.**
|
|
1901
|
+
## SEMO란?
|
|
1949
1902
|
|
|
1950
|
-
|
|
1903
|
+
**SEMO (Semicolon Orchestrate)** 는 OpenClaw 봇팀과 로컬 Claude Code 세션이
|
|
1904
|
+
**팀 Core DB를 단일 진실 공급원(Single Source of Truth)으로 공유**하는 컨텍스트 동기화 시스템이다.
|
|
1951
1905
|
|
|
1952
1906
|
\`\`\`
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
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
|
|
1958
1914
|
\`\`\`
|
|
1959
1915
|
|
|
1960
|
-
**이
|
|
1916
|
+
**이 CLAUDE.md가 설치된 프로젝트는 OpenClaw 봇팀의 컨텍스트를 실시간으로 공유받는다.**
|
|
1961
1917
|
|
|
1962
1918
|
---
|
|
1963
1919
|
|
|
1964
|
-
##
|
|
1920
|
+
## 자동 동기화
|
|
1965
1921
|
|
|
1966
|
-
|
|
1922
|
+
세션 시작/종료 시 팀 Core DB와 자동 동기화됩니다.
|
|
1967
1923
|
|
|
1968
|
-
|
|
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
|
+
## 설치된 구성
|
|
1969
1951
|
|
|
1970
1952
|
\`\`\`
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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/)
|
|
1975
1960
|
\`\`\`
|
|
1976
1961
|
|
|
1977
|
-
|
|
1962
|
+
---
|
|
1978
1963
|
|
|
1979
|
-
|
|
1964
|
+
## 프로젝트 규칙 (팀이 채워야 함)
|
|
1980
1965
|
|
|
1981
|
-
|
|
1966
|
+
> 아래 섹션은 이 프로젝트 고유의 규칙을 기록하세요.
|
|
1967
|
+
> 팀 공통 규칙은 \`memory/process.md\`에 있습니다.
|
|
1982
1968
|
|
|
1983
|
-
|
|
1969
|
+
### 기술 스택
|
|
1984
1970
|
|
|
1985
|
-
|
|
1971
|
+
<!-- 예: Next.js 14, PostgreSQL, TypeScript strict mode -->
|
|
1986
1972
|
|
|
1987
|
-
|
|
1973
|
+
### 브랜치 전략
|
|
1988
1974
|
|
|
1989
|
-
|
|
1990
|
-
- 코드 작성/수정 → \`implementation-master\` 또는 \`coder\` 스킬
|
|
1991
|
-
- Git 커밋/푸시 → \`git-workflow\` 스킬
|
|
1992
|
-
- 품질 검증 → \`quality-master\` 또는 \`verify\` 스킬
|
|
1993
|
-
- 명세 작성 → \`spec-master\`
|
|
1994
|
-
- 일반 작업 → Orchestrator 분석 후 라우팅
|
|
1975
|
+
<!-- 예: main(prod) / dev(staging) / feat/* -->
|
|
1995
1976
|
|
|
1996
|
-
###
|
|
1977
|
+
### 코딩 컨벤션
|
|
1997
1978
|
|
|
1998
|
-
|
|
1979
|
+
<!-- 예: ESLint airbnb, 함수형 컴포넌트 필수, any 금지 -->
|
|
1999
1980
|
|
|
2000
|
-
|
|
2001
|
-
# 필수 검증 순서
|
|
2002
|
-
npm run lint # 1. ESLint 검사
|
|
2003
|
-
npx tsc --noEmit # 2. TypeScript 타입 체크
|
|
2004
|
-
npm run build # 3. 빌드 검증 (Next.js/TypeScript 프로젝트)
|
|
2005
|
-
\`\`\`
|
|
1981
|
+
### 아키텍처 특이사항
|
|
2006
1982
|
|
|
2007
|
-
|
|
2008
|
-
- \`--no-verify\` 플래그 사용 금지
|
|
2009
|
-
- Quality Gate 우회 시도 거부
|
|
2010
|
-
- "그냥 커밋해줘", "빌드 생략해줘" 등 거부
|
|
1983
|
+
<!-- 예: DB 직접 접근 금지 — 반드시 API route 통해야 함 -->
|
|
2011
1984
|
|
|
2012
1985
|
---
|
|
2013
1986
|
|
|
2014
|
-
##
|
|
2015
|
-
|
|
2016
|
-
### Standard (필수)
|
|
2017
|
-
- **semo-core**: 원칙, 오케스트레이터, 공통 커맨드
|
|
2018
|
-
- **semo-skills**: 13개 통합 스킬
|
|
2019
|
-
- 행동: coder, tester, planner, deployer, writer
|
|
2020
|
-
- 운영: memory, notify-slack, feedback, version-updater, semo-help, semo-architecture-checker, circuit-breaker, list-bugs
|
|
1987
|
+
## Quality Gate
|
|
2021
1988
|
|
|
2022
|
-
|
|
1989
|
+
코드 변경 커밋 전 필수:
|
|
2023
1990
|
|
|
2024
|
-
\`\`\`
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
│ ├── context.md # 프로젝트 상태
|
|
2029
|
-
│ ├── decisions.md # 아키텍처 결정
|
|
2030
|
-
│ └── rules/ # 프로젝트별 규칙
|
|
2031
|
-
├── agents → semo-system/semo-core/agents
|
|
2032
|
-
├── skills → semo-system/semo-skills
|
|
2033
|
-
└── commands/SEMO → semo-system/semo-core/commands/SEMO
|
|
2034
|
-
|
|
2035
|
-
semo-system/ # White Box (읽기 전용)
|
|
2036
|
-
├── semo-core/ # Layer 0: 원칙, 오케스트레이션
|
|
2037
|
-
└── semo-skills/ # Layer 1: 통합 스킬
|
|
1991
|
+
\`\`\`bash
|
|
1992
|
+
npm run lint # ESLint
|
|
1993
|
+
npx tsc --noEmit # TypeScript
|
|
1994
|
+
npm run build # 빌드 검증
|
|
2038
1995
|
\`\`\`
|
|
2039
1996
|
|
|
2040
|
-
|
|
1997
|
+
\`--no-verify\` 사용 금지.
|
|
1998
|
+
|
|
1999
|
+
---
|
|
2000
|
+
|
|
2001
|
+
## 슬래시 커맨드
|
|
2041
2002
|
|
|
2042
2003
|
| 커맨드 | 설명 |
|
|
2043
2004
|
|--------|------|
|
|
2044
2005
|
| \`/SEMO:help\` | 도움말 |
|
|
2045
2006
|
| \`/SEMO:feedback\` | 피드백 제출 |
|
|
2046
|
-
| \`/SEMO:
|
|
2047
|
-
| \`/SEMO:onboarding\` | 온보딩 가이드 |
|
|
2048
|
-
| \`/SEMO:dry-run {프롬프트}\` | 명령 검증 (라우팅 시뮬레이션) |
|
|
2049
|
-
|
|
2050
|
-
## Context Mesh 사용
|
|
2051
|
-
|
|
2052
|
-
SEMO는 \`.claude/memory/\`를 통해 세션 간 컨텍스트를 유지합니다:
|
|
2007
|
+
| \`/SEMO:health\` | 환경 헬스체크 |
|
|
2053
2008
|
|
|
2054
|
-
|
|
2055
|
-
- **decisions.md**: 아키텍처 결정 기록 (ADR)
|
|
2056
|
-
- **rules/**: 프로젝트별 커스텀 규칙
|
|
2057
|
-
|
|
2058
|
-
memory 스킬이 자동으로 이 파일들을 관리합니다.
|
|
2009
|
+
---
|
|
2059
2010
|
|
|
2060
|
-
##
|
|
2011
|
+
## 복구 명령어
|
|
2061
2012
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2013
|
+
\`\`\`bash
|
|
2014
|
+
semo doctor # 환경 진단 (DB 연결, 설치 상태)
|
|
2015
|
+
semo config db # DB URL 재설정
|
|
2016
|
+
semo context sync # memory/ 수동 최신화
|
|
2017
|
+
semo bots status # 봇 상태 조회
|
|
2018
|
+
\`\`\`
|
|
2064
2019
|
`;
|
|
2065
2020
|
fs.writeFileSync(claudeMdPath, claudeMdContent);
|
|
2066
2021
|
console.log(chalk_1.default.green("✓ .claude/CLAUDE.md 생성됨"));
|
|
@@ -3001,6 +2956,7 @@ ontoCmd
|
|
|
3001
2956
|
(0, context_1.registerContextCommands)(program);
|
|
3002
2957
|
(0, bots_1.registerBotsCommands)(program);
|
|
3003
2958
|
(0, get_1.registerGetCommands)(program);
|
|
2959
|
+
(0, sessions_1.registerSessionsCommands)(program);
|
|
3004
2960
|
// === semo skills — DB 시딩 ===
|
|
3005
2961
|
/**
|
|
3006
2962
|
* SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
|