@team-semicolon/semo-cli 4.1.5 → 4.2.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.
- package/README.md +3 -4
- package/dist/commands/audit.d.ts +27 -0
- package/dist/commands/audit.js +338 -0
- package/dist/commands/bots.js +524 -24
- package/dist/commands/context.d.ts +14 -3
- package/dist/commands/context.js +192 -113
- package/dist/commands/db.d.ts +9 -0
- package/dist/commands/db.js +189 -0
- package/dist/commands/get.d.ts +1 -2
- package/dist/commands/get.js +24 -116
- package/dist/commands/sessions.d.ts +2 -1
- package/dist/commands/sessions.js +31 -62
- package/dist/commands/skill-sync.d.ts +28 -0
- package/dist/commands/skill-sync.js +111 -0
- package/dist/commands/skill-sync.test.d.ts +16 -0
- package/dist/commands/skill-sync.test.js +186 -0
- package/dist/database.d.ts +41 -3
- package/dist/database.js +128 -554
- package/dist/env-parser.d.ts +5 -0
- package/dist/env-parser.js +27 -0
- package/dist/global-cache.d.ts +12 -0
- package/dist/global-cache.js +184 -0
- package/dist/index.js +352 -817
- package/dist/kb.d.ts +24 -39
- package/dist/kb.js +121 -175
- package/package.json +1 -1
package/dist/commands/context.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* semo context —
|
|
3
|
+
* semo context — 스킬/캐시/크론잡 동기화
|
|
4
4
|
*
|
|
5
|
-
* sync:
|
|
6
|
-
* push: .claude/memory
|
|
5
|
+
* sync: DB → 글로벌 캐시 (skills/commands/agents) + 스킬 DB 동기화 + 크론잡
|
|
6
|
+
* push: .claude/memory/<domain>.md → DB (deprecated — kb_upsert MCP 도구로 대체)
|
|
7
|
+
*
|
|
8
|
+
* [v4.2.0] KB→md 파일 생성 제거 — semo-kb MCP 서버로 통일
|
|
7
9
|
*/
|
|
8
10
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
11
|
if (k2 === undefined) k2 = k;
|
|
@@ -42,24 +44,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
42
44
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
45
|
};
|
|
44
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.syncCronJobs = syncCronJobs;
|
|
45
48
|
exports.registerContextCommands = registerContextCommands;
|
|
46
49
|
const chalk_1 = __importDefault(require("chalk"));
|
|
47
50
|
const ora_1 = __importDefault(require("ora"));
|
|
48
51
|
const fs = __importStar(require("fs"));
|
|
49
52
|
const path = __importStar(require("path"));
|
|
53
|
+
const os = __importStar(require("os"));
|
|
50
54
|
const database_1 = require("../database");
|
|
51
55
|
const kb_1 = require("../kb");
|
|
56
|
+
const skill_sync_1 = require("./skill-sync");
|
|
57
|
+
const global_cache_1 = require("../global-cache");
|
|
52
58
|
// ============================================================
|
|
53
59
|
// Memory file mapping
|
|
54
60
|
// ============================================================
|
|
55
61
|
const MEMORY_DIR = ".claude/memory";
|
|
56
62
|
// --out-dir 로 override 가능 (OpenClaw 봇 workspace 경로 지원)
|
|
63
|
+
// 기본값: ~/.claude/memory/ (글로벌 — 모든 프로젝트에서 공유)
|
|
57
64
|
function resolveMemoryDir(outDir) {
|
|
58
65
|
if (outDir) {
|
|
59
66
|
// 절대경로 또는 ~ 경로 처리
|
|
60
67
|
return outDir.replace(/^~/, require("os").homedir());
|
|
61
68
|
}
|
|
62
|
-
return path.join(
|
|
69
|
+
return path.join(require("os").homedir(), MEMORY_DIR);
|
|
63
70
|
}
|
|
64
71
|
const KB_DOMAIN_MAP = {
|
|
65
72
|
team: "team.md",
|
|
@@ -75,78 +82,82 @@ function ensureMemoryDir(resolvedDir) {
|
|
|
75
82
|
fs.mkdirSync(resolvedDir, { recursive: true });
|
|
76
83
|
return resolvedDir;
|
|
77
84
|
}
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
function botStatusToMarkdown(rows) {
|
|
94
|
-
if (rows.length === 0) {
|
|
95
|
-
return "# Bots\n\n_No bot status data._\n";
|
|
85
|
+
function parseCronJobsFile(filePath) {
|
|
86
|
+
try {
|
|
87
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
88
|
+
const data = JSON.parse(content);
|
|
89
|
+
const jobs = data.jobs || [];
|
|
90
|
+
return jobs.map((job) => ({
|
|
91
|
+
jobId: job.jobId || job.id,
|
|
92
|
+
name: job.name || "",
|
|
93
|
+
schedule: job.schedule || {},
|
|
94
|
+
enabled: job.enabled !== false,
|
|
95
|
+
lastRun: job.lastRun || null,
|
|
96
|
+
nextRun: job.nextRun || null,
|
|
97
|
+
sessionTarget: job.sessionTarget || "main",
|
|
98
|
+
payload: job.payload || null,
|
|
99
|
+
}));
|
|
96
100
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`> 자동 생성: semo context sync (${new Date().toISOString()})\n`,
|
|
100
|
-
"| Bot | 이름 | 역할 | Status | Last Active | Sessions |",
|
|
101
|
-
"|-----|------|------|--------|-------------|----------|",
|
|
102
|
-
];
|
|
103
|
-
for (const bot of rows) {
|
|
104
|
-
const status = bot.status === "online" ? "🟢 online" : "🔴 offline";
|
|
105
|
-
const lastActive = bot.last_active ? new Date(bot.last_active).toLocaleString("ko-KR") : "-";
|
|
106
|
-
const displayName = [bot.emoji, bot.name].filter(Boolean).join(" ") || bot.bot_id;
|
|
107
|
-
lines.push(`| ${bot.bot_id} | ${displayName} | ${bot.role || "-"} | ${status} | ${lastActive} | ${bot.session_count} |`);
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
108
103
|
}
|
|
109
|
-
return lines.join("\n") + "\n";
|
|
110
104
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
function getOpenClawBotIds() {
|
|
106
|
+
const homeDir = os.homedir();
|
|
107
|
+
try {
|
|
108
|
+
return fs.readdirSync(homeDir)
|
|
109
|
+
.filter(f => f.startsWith(".openclaw-"))
|
|
110
|
+
.map(f => f.replace(".openclaw-", ""));
|
|
114
111
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
`> 자동 생성: semo context sync (${new Date().toISOString()})\n`,
|
|
118
|
-
];
|
|
119
|
-
for (const d of domains) {
|
|
120
|
-
lines.push(`\n## ${d.domain} (v${d.version})\n`);
|
|
121
|
-
if (d.description)
|
|
122
|
-
lines.push(`${d.description}\n`);
|
|
123
|
-
lines.push("```json");
|
|
124
|
-
lines.push(JSON.stringify(d.schema, null, 2));
|
|
125
|
-
lines.push("```\n");
|
|
112
|
+
catch {
|
|
113
|
+
return [];
|
|
126
114
|
}
|
|
127
|
-
return lines.join("\n");
|
|
128
115
|
}
|
|
129
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Collect cron jobs from all ~/.openclaw-* directories and upsert to semo.bot_cron_jobs.
|
|
118
|
+
* Uses DELETE + INSERT per bot (same pattern as sync-agent).
|
|
119
|
+
*/
|
|
120
|
+
async function syncCronJobs(pool) {
|
|
121
|
+
const homeDir = os.homedir();
|
|
122
|
+
const botIds = getOpenClawBotIds();
|
|
123
|
+
let totalJobs = 0;
|
|
124
|
+
let syncedBots = 0;
|
|
130
125
|
const client = await pool.connect();
|
|
131
126
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
127
|
+
for (const botId of botIds) {
|
|
128
|
+
const cronPath = path.join(homeDir, `.openclaw-${botId}`, "cron", "jobs.json");
|
|
129
|
+
const jobs = parseCronJobsFile(cronPath);
|
|
130
|
+
// Always delete old entries (handles removed jobs)
|
|
131
|
+
await client.query("DELETE FROM semo.bot_cron_jobs WHERE bot_id = $1", [botId]);
|
|
132
|
+
for (const job of jobs) {
|
|
133
|
+
await client.query(`INSERT INTO semo.bot_cron_jobs
|
|
134
|
+
(bot_id, job_id, name, schedule, enabled, last_run, next_run, session_target, payload, synced_at)
|
|
135
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())`, [
|
|
136
|
+
botId,
|
|
137
|
+
job.jobId,
|
|
138
|
+
job.name,
|
|
139
|
+
JSON.stringify(job.schedule),
|
|
140
|
+
job.enabled,
|
|
141
|
+
job.lastRun,
|
|
142
|
+
job.nextRun,
|
|
143
|
+
job.sessionTarget,
|
|
144
|
+
job.payload ? JSON.stringify(job.payload) : null,
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
totalJobs += jobs.length;
|
|
148
|
+
if (jobs.length > 0)
|
|
149
|
+
syncedBots++;
|
|
150
|
+
}
|
|
141
151
|
}
|
|
142
152
|
finally {
|
|
143
153
|
client.release();
|
|
144
154
|
}
|
|
155
|
+
return { bots: syncedBots, jobs: totalJobs };
|
|
145
156
|
}
|
|
146
157
|
// ============================================================
|
|
147
158
|
// Markdown → KBEntry parser (for push)
|
|
148
159
|
// ============================================================
|
|
149
|
-
function
|
|
160
|
+
function parseMarkdownSections(content, domain) {
|
|
150
161
|
const entries = [];
|
|
151
162
|
// Split by h2 sections
|
|
152
163
|
const sections = content.split(/\n##\s+/);
|
|
@@ -159,7 +170,7 @@ function parseDecisionsMarkdown(content) {
|
|
|
159
170
|
const body = section.substring(firstNewline + 1).trim();
|
|
160
171
|
if (key && body) {
|
|
161
172
|
entries.push({
|
|
162
|
-
domain
|
|
173
|
+
domain,
|
|
163
174
|
key,
|
|
164
175
|
content: body,
|
|
165
176
|
created_by: "claude-context-push",
|
|
@@ -168,22 +179,21 @@ function parseDecisionsMarkdown(content) {
|
|
|
168
179
|
}
|
|
169
180
|
return entries;
|
|
170
181
|
}
|
|
182
|
+
// [v4.2.0] digestToMarkdown 제거 — MCP kb_digest로 대체
|
|
171
183
|
// ============================================================
|
|
172
184
|
// Commands
|
|
173
185
|
// ============================================================
|
|
174
186
|
function registerContextCommands(program) {
|
|
175
187
|
const ctxCmd = program
|
|
176
188
|
.command("context")
|
|
177
|
-
.description("
|
|
189
|
+
.description("스킬/캐시/크론잡 동기화 (KB는 semo-kb MCP 서버)");
|
|
178
190
|
// ── semo context sync ──────────────────────────────────────
|
|
179
191
|
ctxCmd
|
|
180
192
|
.command("sync")
|
|
181
|
-
.description("
|
|
182
|
-
.option("--
|
|
183
|
-
.option("--
|
|
184
|
-
.option("--no-
|
|
185
|
-
.option("--no-ontology", "ontology 동기화 건너뜀")
|
|
186
|
-
.option("--out-dir <path>", "메모리 파일 출력 경로 (기본: .claude/memory/). OpenClaw 봇 workspace 지원용")
|
|
193
|
+
.description("스킬/에이전트/캐시 동기화 + 크론잡 (KB는 semo-kb MCP 서버 사용)")
|
|
194
|
+
.option("--no-skills", "스킬 파일 → DB 동기화 건너뜀")
|
|
195
|
+
.option("--out-dir <path>", "캐시 파일 출력 경로 (기본: .claude/memory/)")
|
|
196
|
+
.option("--no-global-cache", "글로벌 캐시(skills/commands/agents) 동기화 건너뜀")
|
|
187
197
|
.action(async (options) => {
|
|
188
198
|
const spinner = (0, ora_1.default)("context sync 시작...").start();
|
|
189
199
|
const connected = await (0, database_1.isDbConnected)();
|
|
@@ -194,40 +204,75 @@ function registerContextCommands(program) {
|
|
|
194
204
|
}
|
|
195
205
|
const pool = (0, database_1.getPool)();
|
|
196
206
|
const memDir = ensureMemoryDir(resolveMemoryDir(options.outDir));
|
|
197
|
-
let written = 0;
|
|
198
207
|
try {
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
// [v4.2.0] KB→md 파일 생성 제거 — MCP kb_search/kb_list/kb_bot_status/kb_ontology로 대체
|
|
209
|
+
// 기존 memory/*.md (team, projects, decisions, infra, process, bots, ontology) 파일은
|
|
210
|
+
// semo-kb MCP 서버가 실시간 DB 조회로 대체합니다.
|
|
211
|
+
// 1. 스킬 파일 → skill_definitions DB 동기화
|
|
212
|
+
if (options.skills !== false) {
|
|
213
|
+
const semoSystemDir = path.join(process.cwd(), "semo-system");
|
|
214
|
+
if (fs.existsSync(semoSystemDir)) {
|
|
215
|
+
spinner.text = "skills 동기화...";
|
|
216
|
+
const client = await pool.connect();
|
|
217
|
+
try {
|
|
218
|
+
await client.query("BEGIN");
|
|
219
|
+
await (0, skill_sync_1.syncSkillsToDB)(client, semoSystemDir);
|
|
220
|
+
await client.query("COMMIT");
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
await client.query("ROLLBACK").catch(() => { });
|
|
224
|
+
// 스킬 동기화 실패는 무시 — context sync의 핵심은 memory/ 파일
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
client.release();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// 2. DB → 글로벌 캐시 (skills/commands/agents → ~/.claude/)
|
|
232
|
+
if (options.globalCache !== false) {
|
|
233
|
+
spinner.text = "글로벌 캐시 동기화 (skills/commands/agents)...";
|
|
203
234
|
try {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
const content = kbEntriesToMarkdown(domain, shared);
|
|
207
|
-
fs.writeFileSync(path.join(memDir, filename), content);
|
|
208
|
-
written++;
|
|
235
|
+
const cacheResult = await (0, global_cache_1.syncGlobalCache)();
|
|
236
|
+
console.log(chalk_1.default.green(` ✓ 글로벌 캐시: skills(${cacheResult.skills}) commands(${cacheResult.commands}) agents(${cacheResult.agents})`));
|
|
209
237
|
}
|
|
210
|
-
catch {
|
|
211
|
-
//
|
|
238
|
+
catch (cacheErr) {
|
|
239
|
+
// DB 실패 시 기존 파일 유지 (비치명적)
|
|
240
|
+
console.log(chalk_1.default.yellow(` ⚠ 글로벌 캐시 동기화 실패 (기존 파일 유지): ${cacheErr}`));
|
|
212
241
|
}
|
|
213
242
|
}
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
spinner.text = "
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
243
|
+
// 3. 크론잡 동기화 (local → DB)
|
|
244
|
+
try {
|
|
245
|
+
spinner.text = "크론잡 동기화...";
|
|
246
|
+
const cronResult = await syncCronJobs(pool);
|
|
247
|
+
if (cronResult.jobs > 0) {
|
|
248
|
+
console.log(chalk_1.default.green(` ✓ 크론잡: ${cronResult.bots}개 봇, ${cronResult.jobs}개 잡 동기화`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// 크론잡 동기화 실패는 비치명적
|
|
253
|
+
}
|
|
254
|
+
// [v4.2.0] KB Digest 제거 — MCP kb_digest로 대체
|
|
255
|
+
// 4. MCP 서버 자동 등록 (.claude/settings.json)
|
|
256
|
+
try {
|
|
257
|
+
const semoRoot = process.cwd();
|
|
258
|
+
const settingsPath = path.join(memDir, "..", "settings.json");
|
|
259
|
+
if (fs.existsSync(settingsPath)) {
|
|
260
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
261
|
+
if (!settings.mcpServers?.["semo-kb"]) {
|
|
262
|
+
settings.mcpServers = settings.mcpServers || {};
|
|
263
|
+
settings.mcpServers["semo-kb"] = {
|
|
264
|
+
command: "node",
|
|
265
|
+
args: [path.join(semoRoot, "packages/mcp-kb/dist/index.js")],
|
|
266
|
+
};
|
|
267
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
268
|
+
console.log(chalk_1.default.green(" ✓ semo-kb MCP 서버 자동 등록"));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
221
271
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
spinner.text = "ontology 동기화...";
|
|
225
|
-
const domains2 = await (0, kb_1.ontoList)(pool);
|
|
226
|
-
const ontoContent = ontologyToMarkdown(domains2);
|
|
227
|
-
fs.writeFileSync(path.join(memDir, "ontology.md"), ontoContent);
|
|
228
|
-
written++;
|
|
272
|
+
catch {
|
|
273
|
+
// MCP 자동 등록 실패는 비치명적
|
|
229
274
|
}
|
|
230
|
-
spinner.succeed(
|
|
275
|
+
spinner.succeed("context sync 완료 — 스킬/캐시/크론잡 동기화");
|
|
231
276
|
console.log(chalk_1.default.gray(` 저장 위치: ${memDir}`));
|
|
232
277
|
}
|
|
233
278
|
catch (err) {
|
|
@@ -240,28 +285,35 @@ function registerContextCommands(program) {
|
|
|
240
285
|
// ── semo context push ──────────────────────────────────────
|
|
241
286
|
ctxCmd
|
|
242
287
|
.command("push")
|
|
243
|
-
.description(".claude/memory
|
|
244
|
-
.option("--domain <name>", "push할 도메인 (기본: decision)", "decision")
|
|
288
|
+
.description(".claude/memory/<domain>.md → Core DB (semo.knowledge_base)")
|
|
289
|
+
.option("--domain <name>", "push할 도메인 (쉼표 구분 가능, 기본: decision)", "decision")
|
|
245
290
|
.option("--dry-run", "실제 push 없이 변경사항만 미리보기")
|
|
246
291
|
.option("--out-dir <path>", "메모리 파일 경로 (기본: .claude/memory/). OpenClaw 봇 workspace 지원용")
|
|
247
292
|
.action(async (options) => {
|
|
293
|
+
console.log(chalk_1.default.yellow("⚠️ [deprecated] context push는 kb_upsert MCP 도구로 대체 예정입니다."));
|
|
294
|
+
console.log(chalk_1.default.yellow(" 봇/세션에서는 semo-kb MCP 서버의 kb_upsert 도구를 직접 사용하세요.\n"));
|
|
295
|
+
const domains = options.domain.split(",").map((d) => d.trim()).filter(Boolean);
|
|
248
296
|
const memDir = resolveMemoryDir(options.outDir);
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
297
|
+
// 각 도메인별 엔트리 수집
|
|
298
|
+
const allEntries = [];
|
|
299
|
+
for (const domain of domains) {
|
|
300
|
+
const filename = KB_DOMAIN_MAP[domain] || `${domain}.md`;
|
|
301
|
+
const filePath = path.join(memDir, filename);
|
|
302
|
+
if (!fs.existsSync(filePath)) {
|
|
303
|
+
console.log(chalk_1.default.yellow(`⚠️ 파일 없음 (건너뜀): ${MEMORY_DIR}/${filename}`));
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
307
|
+
const entries = parseMarkdownSections(content, domain);
|
|
308
|
+
allEntries.push(...entries);
|
|
255
309
|
}
|
|
256
|
-
|
|
257
|
-
const entries = parseDecisionsMarkdown(content);
|
|
258
|
-
if (entries.length === 0) {
|
|
310
|
+
if (allEntries.length === 0) {
|
|
259
311
|
console.log(chalk_1.default.yellow("⚠️ push할 항목이 없습니다."));
|
|
260
312
|
return;
|
|
261
313
|
}
|
|
262
|
-
console.log(chalk_1.default.cyan(`\n📤 context push: ${
|
|
314
|
+
console.log(chalk_1.default.cyan(`\n📤 context push: ${domains.join(", ")} (${allEntries.length}건)\n`));
|
|
263
315
|
if (options.dryRun) {
|
|
264
|
-
for (const e of
|
|
316
|
+
for (const e of allEntries) {
|
|
265
317
|
console.log(chalk_1.default.gray(` [dry-run] ${e.domain}/${e.key}`));
|
|
266
318
|
}
|
|
267
319
|
return;
|
|
@@ -277,14 +329,41 @@ function registerContextCommands(program) {
|
|
|
277
329
|
let upserted = 0;
|
|
278
330
|
const errors = [];
|
|
279
331
|
try {
|
|
332
|
+
// Domain validation: check all domains against ontology
|
|
333
|
+
const ontologyResult = await client.query("SELECT domain FROM semo.ontology");
|
|
334
|
+
const knownDomains = new Set(ontologyResult.rows.map((r) => r.domain));
|
|
335
|
+
const validEntries = [];
|
|
336
|
+
for (const entry of allEntries) {
|
|
337
|
+
if (knownDomains.has(entry.domain)) {
|
|
338
|
+
validEntries.push(entry);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
errors.push(`${entry.domain}/${entry.key}: 미등록 도메인 '${entry.domain}' (등록된 도메인: ${Array.from(knownDomains).join(', ')})`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (validEntries.length === 0 && errors.length > 0) {
|
|
345
|
+
spinner.fail("모든 엔트리가 도메인 검증에 실패했습니다.");
|
|
346
|
+
errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
|
|
347
|
+
client.release();
|
|
348
|
+
await (0, database_1.closeConnection)();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// Generate embeddings for all valid entries
|
|
352
|
+
spinner.text = "임베딩 생성 중...";
|
|
353
|
+
const texts = validEntries.map(e => `${e.key}: ${e.content}`);
|
|
354
|
+
const embeddings = await (0, kb_1.generateEmbeddings)(texts);
|
|
280
355
|
await client.query("BEGIN");
|
|
281
|
-
for (
|
|
356
|
+
for (let i = 0; i < validEntries.length; i++) {
|
|
357
|
+
const entry = validEntries[i];
|
|
282
358
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
359
|
+
const embedding = embeddings[i];
|
|
360
|
+
const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
|
|
361
|
+
await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by, embedding)
|
|
362
|
+
VALUES ($1, $2, $3, $4, $5, $6::vector)
|
|
285
363
|
ON CONFLICT (domain, key) DO UPDATE SET
|
|
286
364
|
content = EXCLUDED.content,
|
|
287
|
-
metadata = EXCLUDED.metadata
|
|
365
|
+
metadata = EXCLUDED.metadata,
|
|
366
|
+
embedding = COALESCE(EXCLUDED.embedding, semo.knowledge_base.embedding)`, [entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by, embeddingStr]);
|
|
288
367
|
upserted++;
|
|
289
368
|
}
|
|
290
369
|
catch (err) {
|
|
@@ -292,7 +371,7 @@ function registerContextCommands(program) {
|
|
|
292
371
|
}
|
|
293
372
|
}
|
|
294
373
|
await client.query("COMMIT");
|
|
295
|
-
spinner.succeed(`push 완료: ${upserted}건
|
|
374
|
+
spinner.succeed(`push 완료: ${upserted}건 업서트 (임베딩 ${process.env.OPENAI_API_KEY ? '생성됨' : '건너뜀'})`);
|
|
296
375
|
if (errors.length > 0) {
|
|
297
376
|
errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
|
|
298
377
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semo db — 데이터베이스 관리
|
|
3
|
+
*
|
|
4
|
+
* semo db migrate — 마이그레이션 실행
|
|
5
|
+
* semo db migrate --status — 적용된 마이그레이션 목록
|
|
6
|
+
* semo db migrate --dry-run — 미적용 마이그레이션 미리보기
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
export declare function registerDbCommands(program: Command): void;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* semo db — 데이터베이스 관리
|
|
4
|
+
*
|
|
5
|
+
* semo db migrate — 마이그레이션 실행
|
|
6
|
+
* semo db migrate --status — 적용된 마이그레이션 목록
|
|
7
|
+
* semo db migrate --dry-run — 미적용 마이그레이션 미리보기
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
43
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
44
|
+
};
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.registerDbCommands = registerDbCommands;
|
|
47
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
48
|
+
const ora_1 = __importDefault(require("ora"));
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const database_1 = require("../database");
|
|
52
|
+
// ============================================================
|
|
53
|
+
// Migration runner
|
|
54
|
+
// ============================================================
|
|
55
|
+
const MIGRATIONS_DIR = path.resolve(__dirname, "..", "..", "migrations");
|
|
56
|
+
/**
|
|
57
|
+
* schema_migrations 테이블이 없으면 생성
|
|
58
|
+
*/
|
|
59
|
+
async function ensureMigrationsTable() {
|
|
60
|
+
const pool = (0, database_1.getPool)();
|
|
61
|
+
await pool.query(`
|
|
62
|
+
CREATE TABLE IF NOT EXISTS semo.schema_migrations (
|
|
63
|
+
version TEXT PRIMARY KEY,
|
|
64
|
+
applied_at TIMESTAMPTZ DEFAULT NOW()
|
|
65
|
+
);
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 이미 적용된 마이그레이션 버전 목록
|
|
70
|
+
*/
|
|
71
|
+
async function getAppliedMigrations() {
|
|
72
|
+
const pool = (0, database_1.getPool)();
|
|
73
|
+
const { rows } = await pool.query(`SELECT version, applied_at::text FROM semo.schema_migrations ORDER BY version`);
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* migrations/ 디렉터리에서 SQL 파일 목록 (정렬)
|
|
78
|
+
*/
|
|
79
|
+
function getMigrationFiles() {
|
|
80
|
+
if (!fs.existsSync(MIGRATIONS_DIR))
|
|
81
|
+
return [];
|
|
82
|
+
return fs
|
|
83
|
+
.readdirSync(MIGRATIONS_DIR)
|
|
84
|
+
.filter((f) => f.endsWith(".sql"))
|
|
85
|
+
.sort();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 단일 마이그레이션 실행 (트랜잭션)
|
|
89
|
+
*/
|
|
90
|
+
async function runMigration(filename) {
|
|
91
|
+
const pool = (0, database_1.getPool)();
|
|
92
|
+
const client = await pool.connect();
|
|
93
|
+
const filePath = path.join(MIGRATIONS_DIR, filename);
|
|
94
|
+
const sql = fs.readFileSync(filePath, "utf-8");
|
|
95
|
+
const version = filename.replace(/\.sql$/, "");
|
|
96
|
+
try {
|
|
97
|
+
await client.query("BEGIN");
|
|
98
|
+
await client.query(sql);
|
|
99
|
+
await client.query(`INSERT INTO semo.schema_migrations (version) VALUES ($1)`, [version]);
|
|
100
|
+
await client.query("COMMIT");
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
await client.query("ROLLBACK");
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
client.release();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ============================================================
|
|
111
|
+
// Command registration
|
|
112
|
+
// ============================================================
|
|
113
|
+
function registerDbCommands(program) {
|
|
114
|
+
const dbCmd = program
|
|
115
|
+
.command("db")
|
|
116
|
+
.description("데이터베이스 관리");
|
|
117
|
+
dbCmd
|
|
118
|
+
.command("migrate")
|
|
119
|
+
.description("마이그레이션 실행")
|
|
120
|
+
.option("--status", "적용된 마이그레이션 목록만 표시")
|
|
121
|
+
.option("--dry-run", "미적용 마이그레이션 미리보기 (실행하지 않음)")
|
|
122
|
+
.action(async (options) => {
|
|
123
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
124
|
+
if (!connected) {
|
|
125
|
+
console.error(chalk_1.default.red("DB 연결 실패"));
|
|
126
|
+
await (0, database_1.closeConnection)();
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await ensureMigrationsTable();
|
|
131
|
+
const applied = await getAppliedMigrations();
|
|
132
|
+
const appliedSet = new Set(applied.map((r) => r.version));
|
|
133
|
+
const allFiles = getMigrationFiles();
|
|
134
|
+
// --status: 적용 상태만 출력
|
|
135
|
+
if (options.status) {
|
|
136
|
+
if (applied.length === 0) {
|
|
137
|
+
console.log(chalk_1.default.yellow("적용된 마이그레이션 없음"));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.log(chalk_1.default.cyan("적용된 마이그레이션:"));
|
|
141
|
+
for (const r of applied) {
|
|
142
|
+
console.log(` ${chalk_1.default.green("✓")} ${r.version} ${chalk_1.default.gray(r.applied_at)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const pending = allFiles.filter((f) => !appliedSet.has(f.replace(/\.sql$/, "")));
|
|
146
|
+
if (pending.length > 0) {
|
|
147
|
+
console.log(chalk_1.default.yellow(`\n미적용: ${pending.length}개`));
|
|
148
|
+
for (const f of pending) {
|
|
149
|
+
console.log(` ${chalk_1.default.yellow("○")} ${f}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
await (0, database_1.closeConnection)();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// 미적용 마이그레이션 필터
|
|
156
|
+
const pending = allFiles.filter((f) => !appliedSet.has(f.replace(/\.sql$/, "")));
|
|
157
|
+
if (pending.length === 0) {
|
|
158
|
+
console.log(chalk_1.default.green("모든 마이그레이션이 적용됨"));
|
|
159
|
+
await (0, database_1.closeConnection)();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// --dry-run: 미리보기만
|
|
163
|
+
if (options.dryRun) {
|
|
164
|
+
console.log(chalk_1.default.cyan(`미적용 마이그레이션 ${pending.length}개:`));
|
|
165
|
+
for (const f of pending) {
|
|
166
|
+
console.log(` ${chalk_1.default.yellow("○")} ${f}`);
|
|
167
|
+
}
|
|
168
|
+
await (0, database_1.closeConnection)();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// 실행
|
|
172
|
+
const spinner = (0, ora_1.default)(`마이그레이션 실행 중 (${pending.length}개)`).start();
|
|
173
|
+
for (const file of pending) {
|
|
174
|
+
spinner.text = `적용 중: ${file}`;
|
|
175
|
+
await runMigration(file);
|
|
176
|
+
spinner.text = `${chalk_1.default.green("✓")} ${file}`;
|
|
177
|
+
console.log(` ${chalk_1.default.green("✓")} ${file}`);
|
|
178
|
+
}
|
|
179
|
+
spinner.succeed(`마이그레이션 완료 (${pending.length}개 적용)`);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
console.error(chalk_1.default.red(`마이그레이션 실패: ${err}`));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await (0, database_1.closeConnection)();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
package/dist/commands/get.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* semo get <resource> — 세션 중 실시간 DB 쿼리
|
|
3
3
|
*
|
|
4
|
-
* semo get projects [--active]
|
|
4
|
+
* semo get projects [--active] [--format table|json|md]
|
|
5
5
|
* semo get bots [--status online|offline]
|
|
6
6
|
* semo get kb [--domain <d>] [--key <k>] [--search <text>]
|
|
7
7
|
* semo get ontology [--domain <d>]
|
|
8
|
-
* semo get tasks [--project <p>] [--status <s>]
|
|
9
8
|
* semo get sessions [--bot <n>]
|
|
10
9
|
*/
|
|
11
10
|
import { Command } from "commander";
|