@team-semicolon/semo-cli 3.14.1 → 4.0.1

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.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * semo bots — 봇 상태 관리
3
+ *
4
+ * Actual semo.bot_status schema:
5
+ * bot_id, name, emoji, role, last_active, session_count, workspace_path, status, synced_at
6
+ *
7
+ * Actual semo.bot_sessions schema:
8
+ * bot_id, session_key, label, kind, chat_type, last_activity, message_count, synced_at
9
+ */
10
+ import { Command } from "commander";
11
+ export declare function registerBotsCommands(program: Command): void;
@@ -0,0 +1,382 @@
1
+ "use strict";
2
+ /**
3
+ * semo bots — 봇 상태 관리
4
+ *
5
+ * Actual semo.bot_status schema:
6
+ * bot_id, name, emoji, role, last_active, session_count, workspace_path, status, synced_at
7
+ *
8
+ * Actual semo.bot_sessions schema:
9
+ * bot_id, session_key, label, kind, chat_type, last_activity, message_count, synced_at
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.registerBotsCommands = registerBotsCommands;
49
+ const chalk_1 = __importDefault(require("chalk"));
50
+ const ora_1 = __importDefault(require("ora"));
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const database_1 = require("../database");
54
+ function parseIdentityMd(content) {
55
+ const nameMatch = content.match(/\*\*Name:\*\*\s*(.+)/);
56
+ const emojiMatch = content.match(/\*\*Emoji:\*\*\s*(\S+)/);
57
+ const roleMatch = content.match(/\*\*(?:Creature|Role|직책):\*\*\s*(.+)/);
58
+ return {
59
+ name: nameMatch ? nameMatch[1].trim() : null,
60
+ emoji: emojiMatch ? emojiMatch[1].trim() : null,
61
+ role: roleMatch ? roleMatch[1].trim() : null,
62
+ };
63
+ }
64
+ function scanBotWorkspaces(semoSystemDir) {
65
+ const workspacesDir = path.join(semoSystemDir, "bot-workspaces");
66
+ if (!fs.existsSync(workspacesDir))
67
+ return [];
68
+ const bots = [];
69
+ const entries = fs.readdirSync(workspacesDir, { withFileTypes: true });
70
+ for (const entry of entries) {
71
+ if (!entry.isDirectory())
72
+ continue;
73
+ const botId = entry.name;
74
+ const botDir = path.join(workspacesDir, botId);
75
+ // Most recent file mtime
76
+ let lastActive = null;
77
+ try {
78
+ const times = getAllFileMtimes(botDir);
79
+ if (times.length > 0) {
80
+ lastActive = new Date(Math.max(...times.map(t => t.getTime())));
81
+ }
82
+ }
83
+ catch { /* skip */ }
84
+ // Parse IDENTITY.md
85
+ let identity = { name: null, emoji: null, role: null };
86
+ const identityPath = path.join(botDir, "IDENTITY.md");
87
+ if (fs.existsSync(identityPath)) {
88
+ try {
89
+ identity = parseIdentityMd(fs.readFileSync(identityPath, "utf-8"));
90
+ }
91
+ catch { /* skip */ }
92
+ }
93
+ bots.push({ botId, ...identity, lastActive, workspacePath: botDir });
94
+ }
95
+ return bots;
96
+ }
97
+ function getAllFileMtimes(dir, depth = 0) {
98
+ if (depth > 2)
99
+ return [];
100
+ const times = [];
101
+ try {
102
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
103
+ for (const entry of entries) {
104
+ const fullPath = path.join(dir, entry.name);
105
+ if (entry.isFile()) {
106
+ times.push(fs.statSync(fullPath).mtime);
107
+ }
108
+ else if (entry.isDirectory() && !entry.name.startsWith(".")) {
109
+ times.push(...getAllFileMtimes(fullPath, depth + 1));
110
+ }
111
+ }
112
+ }
113
+ catch { /* skip */ }
114
+ return times;
115
+ }
116
+ // ============================================================
117
+ // Command registration
118
+ // ============================================================
119
+ function registerBotsCommands(program) {
120
+ const botsCmd = program
121
+ .command("bots")
122
+ .description("봇 상태 조회 및 관리 (semo.bot_status)");
123
+ // ── semo bots status ────────────────────────────────────────
124
+ botsCmd
125
+ .command("status")
126
+ .description("모든 봇의 현재 상태 조회")
127
+ .option("--status <filter>", "상태 필터 (online|offline)")
128
+ .option("--format <type>", "출력 형식 (table|json)", "table")
129
+ .action(async (options) => {
130
+ const spinner = (0, ora_1.default)("봇 상태 조회 중...").start();
131
+ const connected = await (0, database_1.isDbConnected)();
132
+ if (!connected) {
133
+ spinner.fail("DB 연결 실패");
134
+ await (0, database_1.closeConnection)();
135
+ process.exit(1);
136
+ }
137
+ try {
138
+ const pool = (0, database_1.getPool)();
139
+ const client = await pool.connect();
140
+ let query = `
141
+ SELECT bot_id, name, emoji, role, status,
142
+ last_active::text, session_count, synced_at::text
143
+ FROM semo.bot_status
144
+ `;
145
+ const params = [];
146
+ if (options.status) {
147
+ query += " WHERE status = $1";
148
+ params.push(options.status);
149
+ }
150
+ query += " ORDER BY bot_id";
151
+ const result = await client.query(query, params);
152
+ client.release();
153
+ const bots = result.rows;
154
+ spinner.stop();
155
+ if (options.format === "json") {
156
+ console.log(JSON.stringify(bots, null, 2));
157
+ }
158
+ else {
159
+ console.log(chalk_1.default.cyan.bold("\n🤖 봇 상태\n"));
160
+ if (bots.length === 0) {
161
+ console.log(chalk_1.default.yellow(" 봇 상태 데이터가 없습니다."));
162
+ console.log(chalk_1.default.gray(" 'semo bots sync'로 초기 데이터를 적재하세요."));
163
+ }
164
+ else {
165
+ console.log(chalk_1.default.gray(" 봇 이름 상태 마지막 활동"));
166
+ console.log(chalk_1.default.gray(" " + "─".repeat(75)));
167
+ for (const b of bots) {
168
+ const statusIcon = b.status === "online" ? chalk_1.default.green("● online ") : chalk_1.default.red("○ offline");
169
+ const lastActive = b.last_active
170
+ ? new Date(b.last_active).toLocaleString("ko-KR")
171
+ : "-";
172
+ const displayName = `${b.emoji || ""} ${b.name || b.bot_id}`.trim();
173
+ console.log(` ${b.bot_id.padEnd(16)}${displayName.padEnd(24)}${String(statusIcon).padEnd(12)}${lastActive}`);
174
+ }
175
+ }
176
+ console.log();
177
+ const online = bots.filter(b => b.status === "online").length;
178
+ console.log(chalk_1.default.gray(` 총 ${bots.length}개 봇 (온라인: ${online}개)\n`));
179
+ }
180
+ }
181
+ catch (err) {
182
+ spinner.fail(`조회 실패: ${err}`);
183
+ process.exit(1);
184
+ }
185
+ finally {
186
+ await (0, database_1.closeConnection)();
187
+ }
188
+ });
189
+ // ── semo bots sessions ──────────────────────────────────────
190
+ botsCmd
191
+ .command("sessions")
192
+ .description("봇 세션 히스토리 조회")
193
+ .option("--bot <name>", "특정 봇만")
194
+ .option("--limit <n>", "최대 조회 수", "20")
195
+ .option("--format <type>", "출력 형식 (table|json)", "table")
196
+ .action(async (options) => {
197
+ const spinner = (0, ora_1.default)("세션 조회 중...").start();
198
+ const connected = await (0, database_1.isDbConnected)();
199
+ if (!connected) {
200
+ spinner.fail("DB 연결 실패");
201
+ await (0, database_1.closeConnection)();
202
+ process.exit(1);
203
+ }
204
+ try {
205
+ const pool = (0, database_1.getPool)();
206
+ const client = await pool.connect();
207
+ let query = `
208
+ SELECT bot_id, session_key, label, kind, chat_type,
209
+ last_activity::text, message_count
210
+ FROM semo.bot_sessions
211
+ `;
212
+ const params = [];
213
+ let idx = 1;
214
+ if (options.bot) {
215
+ query += ` WHERE bot_id = $${idx++}`;
216
+ params.push(options.bot);
217
+ }
218
+ query += ` ORDER BY last_activity DESC NULLS LAST LIMIT $${idx++}`;
219
+ params.push(parseInt(options.limit));
220
+ const result = await client.query(query, params);
221
+ client.release();
222
+ const sessions = result.rows;
223
+ spinner.stop();
224
+ if (options.format === "json") {
225
+ console.log(JSON.stringify(sessions, null, 2));
226
+ }
227
+ else {
228
+ console.log(chalk_1.default.cyan.bold("\n📋 봇 세션 히스토리\n"));
229
+ if (sessions.length === 0) {
230
+ console.log(chalk_1.default.yellow(" 세션 데이터가 없습니다."));
231
+ }
232
+ else {
233
+ for (const s of sessions) {
234
+ const lastActivity = s.last_activity
235
+ ? new Date(s.last_activity).toLocaleString("ko-KR")
236
+ : "-";
237
+ console.log(chalk_1.default.cyan(` ${s.bot_id}`) +
238
+ chalk_1.default.gray(` [${s.session_key}]`) +
239
+ (s.label ? chalk_1.default.white(` "${s.label}"`) : "") +
240
+ chalk_1.default.gray(` ${lastActivity} (${s.message_count}msg)`));
241
+ }
242
+ }
243
+ console.log();
244
+ }
245
+ }
246
+ catch (err) {
247
+ spinner.fail(`조회 실패: ${err}`);
248
+ process.exit(1);
249
+ }
250
+ finally {
251
+ await (0, database_1.closeConnection)();
252
+ }
253
+ });
254
+ // ── semo bots sync ──────────────────────────────────────────
255
+ botsCmd
256
+ .command("sync")
257
+ .description("bot-workspaces/ 스캔 → semo.bot_status DB upsert")
258
+ .option("--semo-system <path>", "semo-system 경로 (기본: ./semo-system)")
259
+ .option("--dry-run", "실제 upsert 없이 미리보기")
260
+ .action(async (options) => {
261
+ const cwd = process.cwd();
262
+ const semoSystemDir = options.semoSystem
263
+ ? path.resolve(options.semoSystem)
264
+ : path.join(cwd, "semo-system");
265
+ if (!fs.existsSync(semoSystemDir)) {
266
+ console.log(chalk_1.default.red(`\n❌ semo-system 디렉토리를 찾을 수 없습니다: ${semoSystemDir}`));
267
+ process.exit(1);
268
+ }
269
+ const spinner = (0, ora_1.default)("bot-workspaces 스캔 중...").start();
270
+ const bots = scanBotWorkspaces(semoSystemDir);
271
+ if (bots.length === 0) {
272
+ spinner.warn("봇 워크스페이스가 없습니다.");
273
+ return;
274
+ }
275
+ spinner.text = `${bots.length}개 봇 발견`;
276
+ if (options.dryRun) {
277
+ spinner.stop();
278
+ console.log(chalk_1.default.cyan.bold("\n[dry-run] 감지된 봇:\n"));
279
+ for (const bot of bots) {
280
+ const display = [bot.emoji, bot.name].filter(Boolean).join(" ") || bot.botId;
281
+ console.log(chalk_1.default.gray(` ${bot.botId.padEnd(16)}`) +
282
+ chalk_1.default.white(display.padEnd(24)) +
283
+ chalk_1.default.gray(bot.lastActive?.toLocaleString("ko-KR") || "-"));
284
+ }
285
+ console.log();
286
+ return;
287
+ }
288
+ spinner.text = `${bots.length}개 봇 DB 반영 중...`;
289
+ const connected = await (0, database_1.isDbConnected)();
290
+ if (!connected) {
291
+ spinner.fail("DB 연결 실패");
292
+ await (0, database_1.closeConnection)();
293
+ process.exit(1);
294
+ }
295
+ const pool = (0, database_1.getPool)();
296
+ const client = await pool.connect();
297
+ let upserted = 0;
298
+ const errors = [];
299
+ try {
300
+ await client.query("BEGIN");
301
+ for (const bot of bots) {
302
+ try {
303
+ await client.query(`INSERT INTO semo.bot_status
304
+ (bot_id, name, emoji, role, status, last_active, workspace_path, synced_at)
305
+ VALUES ($1, $2, $3, $4, 'offline', $5, $6, NOW())
306
+ ON CONFLICT (bot_id) DO UPDATE SET
307
+ name = COALESCE(EXCLUDED.name, semo.bot_status.name),
308
+ emoji = COALESCE(EXCLUDED.emoji, semo.bot_status.emoji),
309
+ role = COALESCE(EXCLUDED.role, semo.bot_status.role),
310
+ last_active = CASE
311
+ WHEN EXCLUDED.last_active IS NOT NULL
312
+ AND (semo.bot_status.last_active IS NULL
313
+ OR EXCLUDED.last_active > semo.bot_status.last_active)
314
+ THEN EXCLUDED.last_active
315
+ ELSE semo.bot_status.last_active
316
+ END,
317
+ workspace_path = EXCLUDED.workspace_path,
318
+ synced_at = NOW()`, [
319
+ bot.botId,
320
+ bot.name,
321
+ bot.emoji,
322
+ bot.role,
323
+ bot.lastActive?.toISOString() || null,
324
+ bot.workspacePath,
325
+ ]);
326
+ upserted++;
327
+ }
328
+ catch (err) {
329
+ errors.push(`${bot.botId}: ${err}`);
330
+ }
331
+ }
332
+ await client.query("COMMIT");
333
+ spinner.succeed(`bots sync 완료: ${upserted}개 봇 업서트`);
334
+ if (errors.length > 0) {
335
+ errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
336
+ }
337
+ }
338
+ catch (err) {
339
+ await client.query("ROLLBACK");
340
+ spinner.fail(`sync 실패: ${err}`);
341
+ process.exit(1);
342
+ }
343
+ finally {
344
+ client.release();
345
+ await (0, database_1.closeConnection)();
346
+ }
347
+ });
348
+ // ── semo bots set-status ─────────────────────────────────────
349
+ botsCmd
350
+ .command("set-status <bot_id> <status>")
351
+ .description("봇 온라인 상태 수동 설정 (online|offline)")
352
+ .action(async (botId, status) => {
353
+ if (status !== "online" && status !== "offline") {
354
+ console.log(chalk_1.default.red("❌ status는 'online' 또는 'offline'만 가능합니다."));
355
+ process.exit(1);
356
+ }
357
+ const connected = await (0, database_1.isDbConnected)();
358
+ if (!connected) {
359
+ console.log(chalk_1.default.red("❌ DB 연결 실패"));
360
+ await (0, database_1.closeConnection)();
361
+ process.exit(1);
362
+ }
363
+ try {
364
+ const pool = (0, database_1.getPool)();
365
+ const client = await pool.connect();
366
+ await client.query(`INSERT INTO semo.bot_status (bot_id, status, synced_at)
367
+ VALUES ($1, $2, NOW())
368
+ ON CONFLICT (bot_id) DO UPDATE SET
369
+ status = EXCLUDED.status,
370
+ synced_at = NOW()`, [botId, status]);
371
+ client.release();
372
+ console.log(chalk_1.default.green(`✔ ${botId} → ${status}`));
373
+ }
374
+ catch (err) {
375
+ console.log(chalk_1.default.red(`❌ 실패: ${err}`));
376
+ process.exit(1);
377
+ }
378
+ finally {
379
+ await (0, database_1.closeConnection)();
380
+ }
381
+ });
382
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * semo context — DB ↔ .claude/memory/ 동기화
3
+ *
4
+ * sync: Core DB → .claude/memory/*.md (KB domains, bot_status, ontology, projects)
5
+ * push: .claude/memory/decisions.md → DB (semo.knowledge_base WHERE domain='decision')
6
+ */
7
+ import { Command } from "commander";
8
+ export declare function registerContextCommands(program: Command): void;