@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.
- package/dist/commands/bots.js +16 -3
- package/dist/commands/sessions.d.ts +21 -8
- package/dist/commands/sessions.js +268 -8
- package/dist/index.js +127 -173
- package/package.json +1 -1
package/dist/commands/bots.js
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
* semo.bot_sessions
|
|
4
|
+
* sync: 각 봇의 OpenClaw 게이트웨이(HTTP API) 또는 로컬 sessions.json에서
|
|
5
|
+
* 세션 데이터를 읽어 semo.bot_sessions에 upsert합니다.
|
|
6
6
|
*
|
|
7
|
-
* Claude Code
|
|
8
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
6
|
-
* semo.bot_sessions
|
|
5
|
+
* sync: 각 봇의 OpenClaw 게이트웨이(HTTP API) 또는 로컬 sessions.json에서
|
|
6
|
+
* 세션 데이터를 읽어 semo.bot_sessions에 upsert합니다.
|
|
7
7
|
*
|
|
8
|
-
* Claude Code
|
|
9
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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
|
|
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
|
|
1897
|
+
> SEMO v${VERSION} 설치됨 (${installDate})
|
|
1944
1898
|
|
|
1945
1899
|
---
|
|
1946
1900
|
|
|
1947
|
-
##
|
|
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
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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
|
-
|
|
1962
|
+
---
|
|
1979
1963
|
|
|
1980
|
-
|
|
1964
|
+
## 프로젝트 규칙 (팀이 채워야 함)
|
|
1981
1965
|
|
|
1982
|
-
|
|
1966
|
+
> 아래 섹션은 이 프로젝트 고유의 규칙을 기록하세요.
|
|
1967
|
+
> 팀 공통 규칙은 \`memory/process.md\`에 있습니다.
|
|
1983
1968
|
|
|
1984
|
-
|
|
1969
|
+
### 기술 스택
|
|
1985
1970
|
|
|
1986
|
-
|
|
1971
|
+
<!-- 예: Next.js 14, PostgreSQL, TypeScript strict mode -->
|
|
1987
1972
|
|
|
1988
|
-
|
|
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
|
-
###
|
|
1977
|
+
### 코딩 컨벤션
|
|
1998
1978
|
|
|
1999
|
-
|
|
1979
|
+
<!-- 예: ESLint airbnb, 함수형 컴포넌트 필수, any 금지 -->
|
|
2000
1980
|
|
|
2001
|
-
|
|
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
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
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:
|
|
2048
|
-
| \`/SEMO:onboarding\` | 온보딩 가이드 |
|
|
2049
|
-
| \`/SEMO:dry-run {프롬프트}\` | 명령 검증 (라우팅 시뮬레이션) |
|
|
2050
|
-
|
|
2051
|
-
## Context Mesh 사용
|
|
2052
|
-
|
|
2053
|
-
SEMO는 \`.claude/memory/\`를 통해 세션 간 컨텍스트를 유지합니다:
|
|
2007
|
+
| \`/SEMO:health\` | 환경 헬스체크 |
|
|
2054
2008
|
|
|
2055
|
-
|
|
2056
|
-
- **decisions.md**: 아키텍처 결정 기록 (ADR)
|
|
2057
|
-
- **rules/**: 프로젝트별 커스텀 규칙
|
|
2058
|
-
|
|
2059
|
-
memory 스킬이 자동으로 이 파일들을 관리합니다.
|
|
2009
|
+
---
|
|
2060
2010
|
|
|
2061
|
-
##
|
|
2011
|
+
## 복구 명령어
|
|
2062
2012
|
|
|
2063
|
-
|
|
2064
|
-
|
|
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 생성됨"));
|