@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,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;