@team-semicolon/semo-cli 3.14.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,348 @@
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
+ }
@@ -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;
@@ -0,0 +1,302 @@
1
+ "use strict";
2
+ /**
3
+ * semo context — DB ↔ .claude/memory/ 동기화
4
+ *
5
+ * sync: Core DB → .claude/memory/*.md (KB domains, bot_status, ontology, projects)
6
+ * push: .claude/memory/decisions.md → DB (semo.knowledge_base WHERE domain='decision')
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __importDefault = (this && this.__importDefault) || function (mod) {
42
+ return (mod && mod.__esModule) ? mod : { "default": mod };
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.registerContextCommands = registerContextCommands;
46
+ const chalk_1 = __importDefault(require("chalk"));
47
+ const ora_1 = __importDefault(require("ora"));
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const database_1 = require("../database");
51
+ const kb_1 = require("../kb");
52
+ // ============================================================
53
+ // Memory file mapping
54
+ // ============================================================
55
+ const MEMORY_DIR = ".claude/memory";
56
+ const KB_DOMAIN_MAP = {
57
+ team: "team.md",
58
+ project: "projects.md",
59
+ decision: "decisions.md",
60
+ infra: "infra.md",
61
+ process: "process.md",
62
+ };
63
+ // ============================================================
64
+ // Helpers
65
+ // ============================================================
66
+ function ensureMemoryDir(cwd) {
67
+ const memDir = path.join(cwd, MEMORY_DIR);
68
+ fs.mkdirSync(memDir, { recursive: true });
69
+ return memDir;
70
+ }
71
+ function kbEntriesToMarkdown(domain, entries) {
72
+ if (entries.length === 0) {
73
+ return `# ${domain}\n\n_No entries._\n`;
74
+ }
75
+ const lines = [`# ${domain}\n`, `> 자동 생성: semo context sync (${new Date().toISOString()})\n`];
76
+ for (const entry of entries) {
77
+ lines.push(`\n## ${entry.key}\n`);
78
+ lines.push(entry.content);
79
+ if (entry.metadata && Object.keys(entry.metadata).length > 0) {
80
+ lines.push(`\n_metadata: ${JSON.stringify(entry.metadata)}_`);
81
+ }
82
+ lines.push("");
83
+ }
84
+ return lines.join("\n");
85
+ }
86
+ function botStatusToMarkdown(rows) {
87
+ if (rows.length === 0) {
88
+ return "# Bots\n\n_No bot status data._\n";
89
+ }
90
+ const lines = [
91
+ "# Bots\n",
92
+ `> 자동 생성: semo context sync (${new Date().toISOString()})\n`,
93
+ "| Bot | 이름 | 역할 | Status | Last Active | Sessions |",
94
+ "|-----|------|------|--------|-------------|----------|",
95
+ ];
96
+ for (const bot of rows) {
97
+ const status = bot.status === "online" ? "🟢 online" : "🔴 offline";
98
+ const lastActive = bot.last_active ? new Date(bot.last_active).toLocaleString("ko-KR") : "-";
99
+ const displayName = [bot.emoji, bot.name].filter(Boolean).join(" ") || bot.bot_id;
100
+ lines.push(`| ${bot.bot_id} | ${displayName} | ${bot.role || "-"} | ${status} | ${lastActive} | ${bot.session_count} |`);
101
+ }
102
+ return lines.join("\n") + "\n";
103
+ }
104
+ function ontologyToMarkdown(domains) {
105
+ if (domains.length === 0) {
106
+ return "# Ontology\n\n_No ontology domains._\n";
107
+ }
108
+ const lines = [
109
+ "# Ontology\n",
110
+ `> 자동 생성: semo context sync (${new Date().toISOString()})\n`,
111
+ ];
112
+ for (const d of domains) {
113
+ lines.push(`\n## ${d.domain} (v${d.version})\n`);
114
+ if (d.description)
115
+ lines.push(`${d.description}\n`);
116
+ lines.push("```json");
117
+ lines.push(JSON.stringify(d.schema, null, 2));
118
+ lines.push("```\n");
119
+ }
120
+ return lines.join("\n");
121
+ }
122
+ async function fetchBotStatus(pool) {
123
+ const client = await pool.connect();
124
+ try {
125
+ const result = await client.query(`
126
+ SELECT bot_id, name, emoji, role, status, last_active::text, session_count
127
+ FROM semo.bot_status
128
+ ORDER BY bot_id
129
+ `);
130
+ return result.rows;
131
+ }
132
+ catch {
133
+ return [];
134
+ }
135
+ finally {
136
+ client.release();
137
+ }
138
+ }
139
+ // ============================================================
140
+ // Markdown → KBEntry parser (for push)
141
+ // ============================================================
142
+ function parseDecisionsMarkdown(content) {
143
+ const entries = [];
144
+ // Split by h2 sections
145
+ const sections = content.split(/\n##\s+/);
146
+ for (let i = 1; i < sections.length; i++) {
147
+ const section = sections[i];
148
+ const firstNewline = section.indexOf("\n");
149
+ if (firstNewline === -1)
150
+ continue;
151
+ const key = section.substring(0, firstNewline).trim();
152
+ const body = section.substring(firstNewline + 1).trim();
153
+ if (key && body) {
154
+ entries.push({
155
+ domain: "decision",
156
+ key,
157
+ content: body,
158
+ created_by: "claude-context-push",
159
+ });
160
+ }
161
+ }
162
+ return entries;
163
+ }
164
+ // ============================================================
165
+ // Commands
166
+ // ============================================================
167
+ function registerContextCommands(program) {
168
+ const ctxCmd = program
169
+ .command("context")
170
+ .description("Core DB ↔ .claude/memory/ 컨텍스트 동기화");
171
+ // ── semo context sync ──────────────────────────────────────
172
+ ctxCmd
173
+ .command("sync")
174
+ .description("Core DB → .claude/memory/ 파일 생성")
175
+ .option("--bot <name>", "봇 ID (bot_status 필터)")
176
+ .option("--domain <name>", "특정 KB 도메인만")
177
+ .option("--no-bots", "bot_status 동기화 건너뜀")
178
+ .option("--no-ontology", "ontology 동기화 건너뜀")
179
+ .action(async (options) => {
180
+ const cwd = process.cwd();
181
+ const spinner = (0, ora_1.default)("context sync 시작...").start();
182
+ const connected = await (0, database_1.isDbConnected)();
183
+ if (!connected) {
184
+ spinner.warn("DB 연결 실패 — context sync 건너뜀");
185
+ await (0, database_1.closeConnection)();
186
+ return;
187
+ }
188
+ const pool = (0, database_1.getPool)();
189
+ const memDir = ensureMemoryDir(cwd);
190
+ let written = 0;
191
+ try {
192
+ // 1. KB domains → memory/*.md
193
+ const domains = options.domain ? [options.domain] : Object.keys(KB_DOMAIN_MAP);
194
+ for (const domain of domains) {
195
+ spinner.text = `KB 동기화: ${domain}...`;
196
+ try {
197
+ const { shared } = await (0, kb_1.kbList)(pool, { domain, limit: 1000 });
198
+ const filename = KB_DOMAIN_MAP[domain] || `${domain}.md`;
199
+ const content = kbEntriesToMarkdown(domain, shared);
200
+ fs.writeFileSync(path.join(memDir, filename), content);
201
+ written++;
202
+ }
203
+ catch {
204
+ // domain may not exist — skip silently
205
+ }
206
+ }
207
+ // 2. bot_status → memory/bots.md
208
+ if (options.bots !== false) {
209
+ spinner.text = "bot_status 동기화...";
210
+ const botRows = await fetchBotStatus(pool);
211
+ const botsContent = botStatusToMarkdown(botRows);
212
+ fs.writeFileSync(path.join(memDir, "bots.md"), botsContent);
213
+ written++;
214
+ }
215
+ // 3. ontology → memory/ontology.md
216
+ if (options.ontology !== false) {
217
+ spinner.text = "ontology 동기화...";
218
+ const domains2 = await (0, kb_1.ontoList)(pool);
219
+ const ontoContent = ontologyToMarkdown(domains2);
220
+ fs.writeFileSync(path.join(memDir, "ontology.md"), ontoContent);
221
+ written++;
222
+ }
223
+ spinner.succeed(`context sync 완료 — ${written}개 파일 업데이트`);
224
+ console.log(chalk_1.default.gray(` 저장 위치: ${MEMORY_DIR}/`));
225
+ }
226
+ catch (err) {
227
+ spinner.fail(`context sync 실패: ${err}`);
228
+ }
229
+ finally {
230
+ await (0, database_1.closeConnection)();
231
+ }
232
+ });
233
+ // ── semo context push ──────────────────────────────────────
234
+ ctxCmd
235
+ .command("push")
236
+ .description(".claude/memory/decisions.md → Core DB (semo.knowledge_base)")
237
+ .option("--domain <name>", "push할 도메인 (기본: decision)", "decision")
238
+ .option("--dry-run", "실제 push 없이 변경사항만 미리보기")
239
+ .action(async (options) => {
240
+ const cwd = process.cwd();
241
+ const memDir = path.join(cwd, MEMORY_DIR);
242
+ const filename = KB_DOMAIN_MAP[options.domain] || `${options.domain}.md`;
243
+ const filePath = path.join(memDir, filename);
244
+ if (!fs.existsSync(filePath)) {
245
+ console.log(chalk_1.default.red(`\n❌ 파일 없음: ${MEMORY_DIR}/${filename}`));
246
+ console.log(chalk_1.default.gray(" semo context sync 먼저 실행하세요."));
247
+ process.exit(1);
248
+ }
249
+ const content = fs.readFileSync(filePath, "utf-8");
250
+ const entries = parseDecisionsMarkdown(content);
251
+ if (entries.length === 0) {
252
+ console.log(chalk_1.default.yellow("⚠️ push할 항목이 없습니다."));
253
+ return;
254
+ }
255
+ console.log(chalk_1.default.cyan(`\n📤 context push: ${options.domain} (${entries.length}건)\n`));
256
+ if (options.dryRun) {
257
+ for (const e of entries) {
258
+ console.log(chalk_1.default.gray(` [dry-run] ${e.domain}/${e.key}`));
259
+ }
260
+ return;
261
+ }
262
+ const spinner = (0, ora_1.default)("DB에 업로드 중...").start();
263
+ const connected = await (0, database_1.isDbConnected)();
264
+ if (!connected) {
265
+ spinner.fail("DB 연결 실패");
266
+ process.exit(1);
267
+ }
268
+ const pool = (0, database_1.getPool)();
269
+ const client = await pool.connect();
270
+ let upserted = 0;
271
+ const errors = [];
272
+ try {
273
+ await client.query("BEGIN");
274
+ for (const entry of entries) {
275
+ try {
276
+ await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by)
277
+ VALUES ($1, $2, $3, $4, $5)
278
+ ON CONFLICT (domain, key) DO UPDATE SET
279
+ content = EXCLUDED.content,
280
+ metadata = EXCLUDED.metadata`, [entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by]);
281
+ upserted++;
282
+ }
283
+ catch (err) {
284
+ errors.push(`${entry.domain}/${entry.key}: ${err}`);
285
+ }
286
+ }
287
+ await client.query("COMMIT");
288
+ spinner.succeed(`push 완료: ${upserted}건 업서트`);
289
+ if (errors.length > 0) {
290
+ errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
291
+ }
292
+ }
293
+ catch (err) {
294
+ await client.query("ROLLBACK");
295
+ spinner.fail(`push 실패: ${err}`);
296
+ }
297
+ finally {
298
+ client.release();
299
+ await (0, database_1.closeConnection)();
300
+ }
301
+ });
302
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * semo get <resource> — 세션 중 실시간 DB 쿼리
3
+ *
4
+ * semo get projects [--active]
5
+ * semo get bots [--status online|offline]
6
+ * semo get kb [--domain <d>] [--key <k>] [--search <text>]
7
+ * semo get ontology [--domain <d>]
8
+ * semo get tasks [--project <p>] [--status <s>]
9
+ * semo get sessions [--bot <n>]
10
+ */
11
+ import { Command } from "commander";
12
+ export declare function registerGetCommands(program: Command): void;