@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.
@@ -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
- const nameMatch = content.match(/\*\*Name:\*\*\s*(.+)/);
57
- const emojiMatch = content.match(/\*\*Emoji:\*\*\s*(\S+)/);
58
- const roleMatch = content.match(/\*\*(?:Creature|Role|직책):\*\*\s*(.+)/);
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: nameMatch ? nameMatch[1].trim() : null,
61
- emoji: emojiMatch ? emojiMatch[1].trim() : null,
62
- role: roleMatch ? roleMatch[1].trim() : null,
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 semoCmd = process.argv[1];
357
- (0, child_process_1.spawnSync)(process.execPath, [semoCmd, "sessions", "sync", "--all"], {
358
- stdio: "inherit",
359
- timeout: 60000,
360
- });
361
- }
362
- catch {
363
- console.log(chalk_1.default.yellow(" ⚠ sessions sync 호출 실패 (무시)"));
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 — DB ↔ .claude/memory/ 동기화
2
+ * semo context — 스킬/캐시/크론잡 동기화
3
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')
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;