@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.
- package/dist/commands/bots.d.ts +11 -0
- package/dist/commands/bots.js +382 -0
- package/dist/commands/context.d.ts +8 -0
- package/dist/commands/context.js +302 -0
- package/dist/commands/get.d.ts +12 -0
- package/dist/commands/get.js +433 -0
- package/dist/database.d.ts +2 -0
- package/dist/database.js +26 -10
- package/dist/index.d.ts +6 -9
- package/dist/index.js +750 -1298
- package/dist/kb.d.ts +134 -0
- package/dist/kb.js +627 -0
- package/package.json +1 -1
|
@@ -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;
|