@team-semicolon/semo-cli 4.1.4 → 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/bots.js
CHANGED
|
@@ -50,16 +50,23 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
50
50
|
const ora_1 = __importDefault(require("ora"));
|
|
51
51
|
const fs = __importStar(require("fs"));
|
|
52
52
|
const path = __importStar(require("path"));
|
|
53
|
-
const child_process_1 = require("child_process");
|
|
54
53
|
const database_1 = require("../database");
|
|
54
|
+
const sessions_1 = require("./sessions");
|
|
55
|
+
const audit_1 = require("./audit");
|
|
56
|
+
const skill_sync_1 = require("./skill-sync");
|
|
57
|
+
const context_1 = require("./context");
|
|
55
58
|
function parseIdentityMd(content) {
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
+
// P2-2: 대소문자 무시, **Key:**/Key: 양쪽 지원, 100자 초과 시 잘못된 파싱으로 간주
|
|
60
|
+
const nameMatch = content.match(/(?:\*\*)?Name:(?:\*\*)?\s*(.+)/i);
|
|
61
|
+
const emojiMatch = content.match(/(?:\*\*)?Emoji:(?:\*\*)?\s*(\S+)/i);
|
|
62
|
+
const roleMatch = content.match(/(?:\*\*)?(?:Creature|Role|직책):(?:\*\*)?\s*(.+)/i);
|
|
63
|
+
const name = nameMatch ? nameMatch[1].trim() : null;
|
|
64
|
+
const emoji = emojiMatch ? emojiMatch[1].trim() : null;
|
|
65
|
+
const role = roleMatch ? roleMatch[1].trim() : null;
|
|
59
66
|
return {
|
|
60
|
-
name:
|
|
61
|
-
emoji:
|
|
62
|
-
role:
|
|
67
|
+
name: name && name.length <= 100 ? name : null,
|
|
68
|
+
emoji: emoji && emoji.length <= 10 ? emoji : null,
|
|
69
|
+
role: role && role.length <= 100 ? role : null,
|
|
63
70
|
};
|
|
64
71
|
}
|
|
65
72
|
function scanBotWorkspaces(semoSystemDir) {
|
|
@@ -308,6 +315,7 @@ function registerBotsCommands(program) {
|
|
|
308
315
|
name = COALESCE(EXCLUDED.name, semo.bot_status.name),
|
|
309
316
|
emoji = COALESCE(EXCLUDED.emoji, semo.bot_status.emoji),
|
|
310
317
|
role = COALESCE(EXCLUDED.role, semo.bot_status.role),
|
|
318
|
+
status = semo.bot_status.status,
|
|
311
319
|
last_active = CASE
|
|
312
320
|
WHEN EXCLUDED.last_active IS NOT NULL
|
|
313
321
|
AND (semo.bot_status.last_active IS NULL
|
|
@@ -331,36 +339,528 @@ function registerBotsCommands(program) {
|
|
|
331
339
|
}
|
|
332
340
|
}
|
|
333
341
|
await client.query("COMMIT");
|
|
334
|
-
// session_count를 bot_sessions 실제 집계로 갱신
|
|
335
|
-
await client.query(`UPDATE semo.bot_status bs
|
|
336
|
-
SET session_count = (
|
|
337
|
-
SELECT COUNT(*) FROM semo.bot_sessions WHERE bot_id = bs.bot_id
|
|
338
|
-
)`);
|
|
339
342
|
spinner.succeed(`bots sync 완료: ${upserted}개 봇 업서트`);
|
|
340
343
|
if (errors.length > 0) {
|
|
341
344
|
errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
|
|
342
345
|
}
|
|
346
|
+
// P2-1: sessions sync 연동 — spawnSync 대신 같은 프로세스에서 직접 호출
|
|
347
|
+
try {
|
|
348
|
+
const botIds = bots.map(b => b.botId);
|
|
349
|
+
console.log(chalk_1.default.gray(" → sessions sync 실행 중..."));
|
|
350
|
+
const sessionsClient = await pool.connect();
|
|
351
|
+
const { total } = await (0, sessions_1.syncBotSessions)(botIds, sessionsClient);
|
|
352
|
+
sessionsClient.release();
|
|
353
|
+
if (total > 0) {
|
|
354
|
+
console.log(chalk_1.default.green(` → sessions sync 완료: ${total}건 upsert`));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
console.log(chalk_1.default.yellow(" ⚠ sessions sync 실패 (무시)"));
|
|
359
|
+
}
|
|
360
|
+
// Cron jobs piggyback — sync 후 크론잡 동기화
|
|
361
|
+
try {
|
|
362
|
+
console.log(chalk_1.default.gray(" → cron sync 실행 중..."));
|
|
363
|
+
const cronResult = await (0, context_1.syncCronJobs)(pool);
|
|
364
|
+
if (cronResult.jobs > 0) {
|
|
365
|
+
console.log(chalk_1.default.green(` → cron sync 완료: ${cronResult.bots}개 봇, ${cronResult.jobs}개 잡`));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
console.log(chalk_1.default.yellow(" ⚠ cron sync 실패 (무시)"));
|
|
370
|
+
}
|
|
371
|
+
// Audit piggyback — sync 후 자동 audit 실행
|
|
372
|
+
try {
|
|
373
|
+
console.log(chalk_1.default.gray(" → audit 실행 중..."));
|
|
374
|
+
let auditResults = bots.map(b => (0, audit_1.auditBot)(b.workspacePath, b.botId));
|
|
375
|
+
// KB 도메인 체크 merge (팀 레벨 — 한 번 조회 후 전체 적용)
|
|
376
|
+
const kbChecks = await (0, audit_1.auditBotKb)(pool);
|
|
377
|
+
auditResults = auditResults.map(r => (0, audit_1.mergeDbChecks)(r, kbChecks));
|
|
378
|
+
const auditClient = await pool.connect();
|
|
379
|
+
await (0, audit_1.storeAuditResults)(auditResults, auditClient);
|
|
380
|
+
auditClient.release();
|
|
381
|
+
const good = auditResults.filter(r => r.rating === "GOOD").length;
|
|
382
|
+
console.log(chalk_1.default.green(` → audit 완료: ${auditResults.length}개 봇 (GOOD: ${good})`));
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
console.log(chalk_1.default.yellow(" ⚠ audit 저장 실패 (무시)"));
|
|
386
|
+
}
|
|
387
|
+
// Skills piggyback — 스킬 파일 → skill_definitions 동기화
|
|
388
|
+
try {
|
|
389
|
+
console.log(chalk_1.default.gray(" → skills sync 실행 중..."));
|
|
390
|
+
const skillClient = await pool.connect();
|
|
391
|
+
try {
|
|
392
|
+
await skillClient.query("BEGIN");
|
|
393
|
+
const result = await (0, skill_sync_1.syncSkillsToDB)(skillClient, semoSystemDir);
|
|
394
|
+
await skillClient.query("COMMIT");
|
|
395
|
+
console.log(chalk_1.default.green(` → skills sync 완료: ${result.total}개 (봇 전용: ${result.botSpecific})`));
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
skillClient.release();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
console.log(chalk_1.default.yellow(" ⚠ skills sync 실패 (무시)"));
|
|
403
|
+
}
|
|
343
404
|
}
|
|
344
405
|
catch (err) {
|
|
345
406
|
await client.query("ROLLBACK");
|
|
346
407
|
spinner.fail(`sync 실패: ${err}`);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
client.release();
|
|
412
|
+
await (0, database_1.closeConnection)();
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
// ── semo bots audit ───────────────────────────────────────────
|
|
416
|
+
botsCmd
|
|
417
|
+
.command("audit")
|
|
418
|
+
.description("봇 워크스페이스 표준 구조 audit")
|
|
419
|
+
.option("--format <type>", "출력 형식 (table|json|slack)", "table")
|
|
420
|
+
.option("--fix", "누락 파일/디렉토리 자동 생성")
|
|
421
|
+
.option("--no-db", "DB 저장 건너뛰기")
|
|
422
|
+
.option("--semo-system <path>", "semo-system 경로 (기본: ./semo-system)")
|
|
423
|
+
.action(async (options) => {
|
|
424
|
+
const cwd = process.cwd();
|
|
425
|
+
const semoSystemDir = options.semoSystem
|
|
426
|
+
? path.resolve(options.semoSystem)
|
|
427
|
+
: path.join(cwd, "semo-system");
|
|
428
|
+
const workspacesDir = path.join(semoSystemDir, "bot-workspaces");
|
|
429
|
+
if (!fs.existsSync(workspacesDir)) {
|
|
430
|
+
console.log(chalk_1.default.red(`\n❌ bot-workspaces 디렉토리를 찾을 수 없습니다: ${workspacesDir}`));
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
const spinner = (0, ora_1.default)("bot-workspaces audit 중...").start();
|
|
434
|
+
// Scan bot directories
|
|
435
|
+
const entries = fs.readdirSync(workspacesDir, { withFileTypes: true });
|
|
436
|
+
const botDirs = entries.filter(e => e.isDirectory());
|
|
437
|
+
if (botDirs.length === 0) {
|
|
438
|
+
spinner.warn("봇 워크스페이스가 없습니다.");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
// Run audit
|
|
442
|
+
const results = botDirs.map(e => {
|
|
443
|
+
const botDir = path.join(workspacesDir, e.name);
|
|
444
|
+
return (0, audit_1.auditBot)(botDir, e.name);
|
|
445
|
+
});
|
|
446
|
+
spinner.stop();
|
|
447
|
+
// --fix
|
|
448
|
+
if (options.fix) {
|
|
449
|
+
let totalFixed = 0;
|
|
450
|
+
for (const r of results) {
|
|
451
|
+
const botDir = path.join(workspacesDir, r.botId);
|
|
452
|
+
const fixed = (0, audit_1.fixBot)(botDir, r.botId, r.checks);
|
|
453
|
+
if (fixed > 0) {
|
|
454
|
+
console.log(chalk_1.default.green(` ✔ ${r.botId}: ${fixed}개 파일/디렉토리 생성`));
|
|
455
|
+
totalFixed += fixed;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (totalFixed > 0) {
|
|
459
|
+
console.log(chalk_1.default.green(`\n총 ${totalFixed}개 수정`));
|
|
460
|
+
// Re-audit after fix
|
|
461
|
+
for (let i = 0; i < results.length; i++) {
|
|
462
|
+
const botDir = path.join(workspacesDir, results[i].botId);
|
|
463
|
+
results[i] = (0, audit_1.auditBot)(botDir, results[i].botId);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// DB sync checks + store
|
|
468
|
+
if (options.db !== false) {
|
|
469
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
470
|
+
if (connected) {
|
|
471
|
+
const pool = (0, database_1.getPool)();
|
|
472
|
+
// Merge DB sync checks into results
|
|
473
|
+
try {
|
|
474
|
+
for (let i = 0; i < results.length; i++) {
|
|
475
|
+
const dbChecks = await (0, audit_1.auditBotDb)(results[i].botId, pool);
|
|
476
|
+
results[i] = (0, audit_1.mergeDbChecks)(results[i], dbChecks);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
console.log(chalk_1.default.yellow(` ⚠ DB sync 체크 실패: ${err}`));
|
|
481
|
+
}
|
|
482
|
+
// Merge KB domain checks (team-level — 한 번 조회 후 전체 적용)
|
|
483
|
+
try {
|
|
484
|
+
const kbChecks = await (0, audit_1.auditBotKb)(pool);
|
|
485
|
+
for (let i = 0; i < results.length; i++) {
|
|
486
|
+
results[i] = (0, audit_1.mergeDbChecks)(results[i], kbChecks);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
console.log(chalk_1.default.yellow(` ⚠ KB 도메인 체크 실패: ${err}`));
|
|
491
|
+
}
|
|
492
|
+
// Store results (separate try — table may not exist yet)
|
|
493
|
+
try {
|
|
494
|
+
const client = await pool.connect();
|
|
495
|
+
try {
|
|
496
|
+
await (0, audit_1.storeAuditResults)(results, client);
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
client.release();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// bot_workspace_audits table may not exist — silent skip
|
|
504
|
+
}
|
|
505
|
+
await (0, database_1.closeConnection)();
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
await (0, database_1.closeConnection)();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Output
|
|
512
|
+
if (options.format === "json") {
|
|
513
|
+
console.log(JSON.stringify(results, null, 2));
|
|
514
|
+
}
|
|
515
|
+
else if (options.format === "slack") {
|
|
516
|
+
console.log((0, audit_1.formatAuditSlack)(results));
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
console.log(chalk_1.default.cyan.bold("\n🔍 Bot Workspace Audit\n"));
|
|
520
|
+
console.log(chalk_1.default.gray(" 봇 Score Rating Passed"));
|
|
521
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(55)));
|
|
522
|
+
for (const r of results) {
|
|
523
|
+
const ratingColor = r.rating === "GOOD" ? chalk_1.default.green :
|
|
524
|
+
r.rating === "NEEDS-WORK" ? chalk_1.default.yellow :
|
|
525
|
+
chalk_1.default.red;
|
|
526
|
+
const passed = r.checks.filter(c => c.passed).length;
|
|
527
|
+
console.log(` ${r.botId.padEnd(16)}${String(r.score).padStart(3)}% ${ratingColor(r.rating.padEnd(12))} ${passed}/${r.checks.length}`);
|
|
528
|
+
}
|
|
529
|
+
const avgScore = Math.round(results.reduce((s, r) => s + r.score, 0) / results.length);
|
|
530
|
+
const good = results.filter(r => r.rating === "GOOD").length;
|
|
531
|
+
console.log(chalk_1.default.gray(`\n ${results.length}개 봇, 평균 ${avgScore}%, GOOD: ${good}개\n`));
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
// ── semo bots seed ──────────────────────────────────────────
|
|
535
|
+
botsCmd
|
|
536
|
+
.command("seed")
|
|
537
|
+
.description("semo-skills + bot-workspaces → skill_definitions / agent_definitions 시딩")
|
|
538
|
+
.option("--semo-system <path>", "semo-system 경로 (기본: ./semo-system)")
|
|
539
|
+
.option("--reset", "시딩 전 기존 데이터 삭제")
|
|
540
|
+
.option("--dry-run", "실제 DB 반영 없이 미리보기")
|
|
541
|
+
.action(async (options) => {
|
|
542
|
+
const cwd = process.cwd();
|
|
543
|
+
const semoSystemDir = options.semoSystem
|
|
544
|
+
? path.resolve(options.semoSystem)
|
|
545
|
+
: path.join(cwd, "semo-system");
|
|
546
|
+
if (!fs.existsSync(semoSystemDir)) {
|
|
547
|
+
console.log(chalk_1.default.red(`\n❌ semo-system 디렉토리를 찾을 수 없습니다: ${semoSystemDir}`));
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
const spinner = (0, ora_1.default)("스킬/에이전트 스캔 중...").start();
|
|
551
|
+
// ─── 1+2. 스킬 스캔 (공통 모듈) ─────────────────────────
|
|
552
|
+
const botSkills = (0, skill_sync_1.scanSkills)(semoSystemDir);
|
|
553
|
+
// ─── 3. 에이전트 스캔 ─────────────────────────────────
|
|
554
|
+
const workspacesDir = path.join(semoSystemDir, "bot-workspaces");
|
|
555
|
+
const agents = [];
|
|
556
|
+
if (fs.existsSync(workspacesDir)) {
|
|
557
|
+
const botEntries = fs.readdirSync(workspacesDir, { withFileTypes: true });
|
|
558
|
+
for (const botEntry of botEntries) {
|
|
559
|
+
if (!botEntry.isDirectory())
|
|
560
|
+
continue;
|
|
561
|
+
const botDir = path.join(workspacesDir, botEntry.name);
|
|
562
|
+
const identityPath = path.join(botDir, "IDENTITY.md");
|
|
563
|
+
if (!fs.existsSync(identityPath))
|
|
564
|
+
continue;
|
|
565
|
+
try {
|
|
566
|
+
const identity = parseIdentityMd(fs.readFileSync(identityPath, "utf-8"));
|
|
567
|
+
// persona_prompt = SOUL.md + \n\n---\n\n + AGENTS.md
|
|
568
|
+
const parts = [];
|
|
569
|
+
const soulPath = path.join(botDir, "SOUL.md");
|
|
570
|
+
if (fs.existsSync(soulPath)) {
|
|
571
|
+
parts.push(fs.readFileSync(soulPath, "utf-8"));
|
|
572
|
+
}
|
|
573
|
+
const agentsPath = path.join(botDir, "AGENTS.md");
|
|
574
|
+
if (fs.existsSync(agentsPath)) {
|
|
575
|
+
parts.push(fs.readFileSync(agentsPath, "utf-8"));
|
|
576
|
+
}
|
|
577
|
+
const personaPrompt = parts.join("\n\n---\n\n");
|
|
578
|
+
agents.push({
|
|
579
|
+
botId: botEntry.name,
|
|
580
|
+
name: identity.name || botEntry.name,
|
|
581
|
+
emoji: identity.emoji,
|
|
582
|
+
role: (identity.role || "custom").substring(0, 50),
|
|
583
|
+
personaPrompt,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
catch { /* skip */ }
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
spinner.stop();
|
|
590
|
+
// ─── 미리보기 출력 ─────────────────────────────────────
|
|
591
|
+
console.log(chalk_1.default.cyan.bold("\n📦 Seed 스캔 결과\n"));
|
|
592
|
+
console.log(chalk_1.default.white(` 봇 전용 스킬 (openclaw): ${botSkills.length}개`));
|
|
593
|
+
console.log(chalk_1.default.white(` 에이전트 (봇): ${agents.length}개`));
|
|
594
|
+
if (agents.length > 0) {
|
|
595
|
+
console.log(chalk_1.default.gray("\n 에이전트:"));
|
|
596
|
+
for (const a of agents) {
|
|
597
|
+
const ownSkills = botSkills.filter(s => s.botId === a.botId);
|
|
598
|
+
console.log(chalk_1.default.gray(` ${a.emoji || "?"} ${a.botId.padEnd(14)}`) +
|
|
599
|
+
chalk_1.default.white(`${a.role}`.substring(0, 40).padEnd(42)) +
|
|
600
|
+
chalk_1.default.gray(`전용 스킬: ${ownSkills.length}`));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (options.dryRun) {
|
|
604
|
+
console.log(chalk_1.default.yellow("\n [dry-run] DB 반영 없이 종료\n"));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
// ─── DB 반영 ──────────────────────────────────────────
|
|
608
|
+
const spinnerDb = (0, ora_1.default)("DB 반영 중...").start();
|
|
609
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
610
|
+
if (!connected) {
|
|
611
|
+
spinnerDb.fail("DB 연결 실패");
|
|
612
|
+
await (0, database_1.closeConnection)();
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
const pool = (0, database_1.getPool)();
|
|
616
|
+
const client = await pool.connect();
|
|
617
|
+
try {
|
|
618
|
+
await client.query("BEGIN");
|
|
619
|
+
// --reset: 기존 데이터 삭제
|
|
620
|
+
if (options.reset) {
|
|
621
|
+
spinnerDb.text = "기존 데이터 삭제 중...";
|
|
622
|
+
await client.query("DELETE FROM agent_definitions");
|
|
623
|
+
await client.query("DELETE FROM skill_definitions WHERE office_id IS NULL");
|
|
624
|
+
}
|
|
625
|
+
// ─── 스킬 시딩 (공통 모듈) ──────────────────────────
|
|
626
|
+
spinnerDb.text = `스킬 ${botSkills.length}개 시딩 중...`;
|
|
627
|
+
await (0, skill_sync_1.syncSkillsToDB)(client, semoSystemDir);
|
|
628
|
+
// ─── 에이전트 시딩 ───────────────────────────────────
|
|
629
|
+
spinnerDb.text = `에이전트 ${agents.length}개 시딩 중...`;
|
|
630
|
+
for (const agent of agents) {
|
|
631
|
+
await client.query(`INSERT INTO agent_definitions (name, role, persona_prompt, package, avatar_config, is_active, office_id)
|
|
632
|
+
VALUES ($1, $2, $3, 'openclaw', $4, true, NULL)
|
|
633
|
+
ON CONFLICT (name, office_id) DO UPDATE SET
|
|
634
|
+
role = EXCLUDED.role,
|
|
635
|
+
persona_prompt = EXCLUDED.persona_prompt,
|
|
636
|
+
avatar_config = EXCLUDED.avatar_config,
|
|
637
|
+
updated_at = NOW()`, [
|
|
638
|
+
agent.botId,
|
|
639
|
+
agent.role,
|
|
640
|
+
agent.personaPrompt,
|
|
641
|
+
JSON.stringify({ emoji: agent.emoji }),
|
|
642
|
+
]);
|
|
643
|
+
}
|
|
644
|
+
// ─── 위임 매트릭스 시딩 ─────────────────────────────
|
|
645
|
+
spinnerDb.text = "위임 매트릭스 시딩 중...";
|
|
646
|
+
const delegationSeeds = [
|
|
647
|
+
{ from: "semiclaw", to: "infraclaw", type: "task", domains: ["infra", "cicd", "deploy", "monitoring"], method: "github_issue" },
|
|
648
|
+
{ from: "semiclaw", to: "designclaw", type: "task", domains: ["ui", "ux", "design", "reference"], method: "github_issue" },
|
|
649
|
+
{ from: "semiclaw", to: "planclaw", type: "task", domains: ["planning", "requirements", "spec"], method: "github_issue" },
|
|
650
|
+
{ from: "semiclaw", to: "reviewclaw", type: "task", domains: ["code_review", "qa", "testing"], method: "github_issue" },
|
|
651
|
+
{ from: "semiclaw", to: "workclaw", type: "task", domains: ["implementation", "dev", "bugfix"], method: "github_issue" },
|
|
652
|
+
{ from: "semiclaw", to: "growthclaw", type: "task", domains: ["marketing", "growth", "analytics", "content"], method: "github_issue" },
|
|
653
|
+
];
|
|
654
|
+
let delegationCount = 0;
|
|
655
|
+
for (const d of delegationSeeds) {
|
|
656
|
+
await client.query(`INSERT INTO semo.bot_delegation
|
|
657
|
+
(from_bot_id, to_bot_id, delegation_type, domains, method)
|
|
658
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
659
|
+
ON CONFLICT (from_bot_id, to_bot_id, delegation_type) DO UPDATE SET
|
|
660
|
+
domains = EXCLUDED.domains,
|
|
661
|
+
method = EXCLUDED.method,
|
|
662
|
+
updated_at = NOW()`, [d.from, d.to, d.type, d.domains, d.method]);
|
|
663
|
+
delegationCount++;
|
|
664
|
+
}
|
|
665
|
+
// ─── 프로토콜 시딩 ──────────────────────────────────
|
|
666
|
+
spinnerDb.text = "프로토콜 메타데이터 시딩 중...";
|
|
667
|
+
const protocolSeeds = [
|
|
668
|
+
{
|
|
669
|
+
key: "task_request_format",
|
|
670
|
+
value: { template: "@{bot} [TASK] {desc}\n[PROJECT] {project}\n[PRIORITY] {priority}\n[ISSUE] {issue}" },
|
|
671
|
+
description: "태스크 요청 메시지 포맷",
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
key: "result_format",
|
|
675
|
+
value: { template: "@SemiClaw [DONE] {desc}\n[RESULT] {summary}\n[ARTIFACTS] {urls}" },
|
|
676
|
+
description: "결과 보고 메시지 포맷",
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
key: "blocked_format",
|
|
680
|
+
value: { template: "@SemiClaw [BLOCKED] {desc}\n[REASON] {reason}\n[NEED] {need}" },
|
|
681
|
+
description: "블로커 보고 메시지 포맷",
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
key: "channel_rules",
|
|
685
|
+
value: { "proj-*": "allowBots", "개발사업팀": "reportOnly" },
|
|
686
|
+
description: "채널별 봇 통신 규칙",
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
key: "general",
|
|
690
|
+
value: { max_roundtrips: 5, hub_bot: "semiclaw" },
|
|
691
|
+
description: "일반 프로토콜 설정",
|
|
692
|
+
},
|
|
693
|
+
];
|
|
694
|
+
let protocolCount = 0;
|
|
695
|
+
for (const p of protocolSeeds) {
|
|
696
|
+
await client.query(`INSERT INTO semo.bot_protocol (key, value, description)
|
|
697
|
+
VALUES ($1, $2, $3)
|
|
698
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
699
|
+
value = EXCLUDED.value,
|
|
700
|
+
description = EXCLUDED.description,
|
|
701
|
+
updated_at = NOW()`, [p.key, JSON.stringify(p.value), p.description]);
|
|
702
|
+
protocolCount++;
|
|
703
|
+
}
|
|
704
|
+
await client.query("COMMIT");
|
|
705
|
+
spinnerDb.succeed("seed 완료");
|
|
706
|
+
console.log(chalk_1.default.green(` ✔ 봇 전용 스킬: ${botSkills.length}개 (metadata.bot_ids)`));
|
|
707
|
+
console.log(chalk_1.default.green(` ✔ 에이전트: ${agents.length}개`));
|
|
708
|
+
console.log(chalk_1.default.green(` ✔ 위임 매트릭스: ${delegationCount}개`));
|
|
709
|
+
console.log(chalk_1.default.green(` ✔ 프로토콜: ${protocolCount}개`));
|
|
710
|
+
console.log();
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
await client.query("ROLLBACK");
|
|
714
|
+
spinnerDb.fail(`seed 실패: ${err}`);
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
finally {
|
|
347
718
|
client.release();
|
|
348
719
|
await (0, database_1.closeConnection)();
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
// ── semo bots cron ──────────────────────────────────────────
|
|
723
|
+
const cronCmd = botsCmd
|
|
724
|
+
.command("cron")
|
|
725
|
+
.description("봇 크론잡 조회 및 동기화");
|
|
726
|
+
cronCmd
|
|
727
|
+
.command("list")
|
|
728
|
+
.description("DB에서 봇 크론잡 조회")
|
|
729
|
+
.option("--bot <name>", "특정 봇만")
|
|
730
|
+
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
731
|
+
.action(async (options) => {
|
|
732
|
+
const spinner = (0, ora_1.default)("크론잡 조회 중...").start();
|
|
733
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
734
|
+
if (!connected) {
|
|
735
|
+
spinner.fail("DB 연결 실패");
|
|
736
|
+
await (0, database_1.closeConnection)();
|
|
349
737
|
process.exit(1);
|
|
350
738
|
}
|
|
351
|
-
client.release();
|
|
352
|
-
await (0, database_1.closeConnection)();
|
|
353
|
-
// sessions sync 연동: DB 연결 반납 후 별도 프로세스로 실행
|
|
354
|
-
console.log(chalk_1.default.gray(" → sessions sync 실행 중..."));
|
|
355
739
|
try {
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
740
|
+
const pool = (0, database_1.getPool)();
|
|
741
|
+
const client = await pool.connect();
|
|
742
|
+
let query = `
|
|
743
|
+
SELECT bot_id, job_id, name, schedule, enabled,
|
|
744
|
+
last_run::text, next_run::text, session_target, synced_at::text
|
|
745
|
+
FROM semo.bot_cron_jobs
|
|
746
|
+
`;
|
|
747
|
+
const params = [];
|
|
748
|
+
if (options.bot) {
|
|
749
|
+
query += " WHERE bot_id = $1";
|
|
750
|
+
params.push(options.bot);
|
|
751
|
+
}
|
|
752
|
+
query += " ORDER BY bot_id, name";
|
|
753
|
+
const result = await client.query(query, params);
|
|
754
|
+
client.release();
|
|
755
|
+
spinner.stop();
|
|
756
|
+
if (options.format === "json") {
|
|
757
|
+
console.log(JSON.stringify(result.rows, null, 2));
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
console.log(chalk_1.default.cyan.bold("\n⏰ 봇 크론잡\n"));
|
|
761
|
+
if (result.rows.length === 0) {
|
|
762
|
+
console.log(chalk_1.default.yellow(" 크론잡 데이터가 없습니다."));
|
|
763
|
+
console.log(chalk_1.default.gray(" 'semo bots cron sync' 또는 'semo context sync'로 동기화하세요."));
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
let currentBot = "";
|
|
767
|
+
for (const row of result.rows) {
|
|
768
|
+
if (row.bot_id !== currentBot) {
|
|
769
|
+
currentBot = row.bot_id;
|
|
770
|
+
console.log(chalk_1.default.white.bold(` ${currentBot}`));
|
|
771
|
+
}
|
|
772
|
+
const status = row.enabled ? chalk_1.default.green("●") : chalk_1.default.red("○");
|
|
773
|
+
const nextRun = row.next_run ? new Date(row.next_run).toLocaleString("ko-KR") : "-";
|
|
774
|
+
console.log(` ${status} ${(row.name || row.job_id).padEnd(30)} next: ${nextRun}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
console.log();
|
|
778
|
+
const enabledCount = result.rows.filter((r) => r.enabled).length;
|
|
779
|
+
console.log(chalk_1.default.gray(` 총 ${result.rows.length}개 잡 (활성: ${enabledCount}개)\n`));
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
spinner.fail(`조회 실패: ${err}`);
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
await (0, database_1.closeConnection)();
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
cronCmd
|
|
791
|
+
.command("sync")
|
|
792
|
+
.description("로컬 크론잡 → DB 수동 동기화")
|
|
793
|
+
.action(async () => {
|
|
794
|
+
const spinner = (0, ora_1.default)("크론잡 동기화 중...").start();
|
|
795
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
796
|
+
if (!connected) {
|
|
797
|
+
spinner.fail("DB 연결 실패");
|
|
798
|
+
await (0, database_1.closeConnection)();
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
const pool = (0, database_1.getPool)();
|
|
803
|
+
const result = await (0, context_1.syncCronJobs)(pool);
|
|
804
|
+
spinner.succeed(`크론잡 동기화 완료: ${result.bots}개 봇, ${result.jobs}개 잡`);
|
|
805
|
+
}
|
|
806
|
+
catch (err) {
|
|
807
|
+
spinner.fail(`동기화 실패: ${err}`);
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
finally {
|
|
811
|
+
await (0, database_1.closeConnection)();
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
// ── semo bots delegation ─────────────────────────────────────
|
|
815
|
+
botsCmd
|
|
816
|
+
.command("delegation")
|
|
817
|
+
.description("봇 간 위임 매트릭스 조회")
|
|
818
|
+
.option("--bot <name>", "특정 봇의 위임 관계만")
|
|
819
|
+
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
820
|
+
.action(async (options) => {
|
|
821
|
+
const spinner = (0, ora_1.default)("위임 매트릭스 조회 중...").start();
|
|
822
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
823
|
+
if (!connected) {
|
|
824
|
+
spinner.fail("DB 연결 실패");
|
|
825
|
+
await (0, database_1.closeConnection)();
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
const delegations = await (0, database_1.getDelegations)(options.bot || undefined);
|
|
830
|
+
spinner.stop();
|
|
831
|
+
if (options.format === "json") {
|
|
832
|
+
console.log(JSON.stringify(delegations, null, 2));
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
console.log(chalk_1.default.cyan.bold("\n🔗 봇 위임 매트릭스\n"));
|
|
836
|
+
if (delegations.length === 0) {
|
|
837
|
+
console.log(chalk_1.default.yellow(" 위임 데이터가 없습니다."));
|
|
838
|
+
console.log(chalk_1.default.gray(" 'semo bots seed'로 위임 매트릭스를 시딩하세요."));
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
let currentFrom = "";
|
|
842
|
+
for (const d of delegations) {
|
|
843
|
+
if (d.from_bot_id !== currentFrom) {
|
|
844
|
+
currentFrom = d.from_bot_id;
|
|
845
|
+
console.log(chalk_1.default.white.bold(` ${currentFrom}`));
|
|
846
|
+
}
|
|
847
|
+
const domains = d.domains.join(", ");
|
|
848
|
+
console.log(chalk_1.default.gray(` → ${d.to_bot_id.padEnd(14)}`) +
|
|
849
|
+
chalk_1.default.white(`[${d.delegation_type}] `) +
|
|
850
|
+
chalk_1.default.cyan(domains) +
|
|
851
|
+
chalk_1.default.gray(` (via ${d.method})`));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
console.log();
|
|
855
|
+
console.log(chalk_1.default.gray(` 총 ${delegations.length}개 위임 관계\n`));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
catch (err) {
|
|
859
|
+
spinner.fail(`조회 실패: ${err}`);
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
finally {
|
|
863
|
+
await (0, database_1.closeConnection)();
|
|
364
864
|
}
|
|
365
865
|
});
|
|
366
866
|
// ── semo bots set-status ─────────────────────────────────────
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* semo context —
|
|
2
|
+
* semo context — 스킬/캐시/크론잡 동기화
|
|
3
3
|
*
|
|
4
|
-
* sync:
|
|
5
|
-
* push: .claude/memory
|
|
4
|
+
* sync: DB → 글로벌 캐시 (skills/commands/agents) + 스킬 DB 동기화 + 크론잡
|
|
5
|
+
* push: .claude/memory/<domain>.md → DB (deprecated — kb_upsert MCP 도구로 대체)
|
|
6
|
+
*
|
|
7
|
+
* [v4.2.0] KB→md 파일 생성 제거 — semo-kb MCP 서버로 통일
|
|
6
8
|
*/
|
|
7
9
|
import { Command } from "commander";
|
|
10
|
+
import { Pool } from "pg";
|
|
11
|
+
/**
|
|
12
|
+
* Collect cron jobs from all ~/.openclaw-* directories and upsert to semo.bot_cron_jobs.
|
|
13
|
+
* Uses DELETE + INSERT per bot (same pattern as sync-agent).
|
|
14
|
+
*/
|
|
15
|
+
export declare function syncCronJobs(pool: Pool): Promise<{
|
|
16
|
+
bots: number;
|
|
17
|
+
jobs: number;
|
|
18
|
+
}>;
|
|
8
19
|
export declare function registerContextCommands(program: Command): void;
|