@team-semicolon/semo-cli 4.8.0 → 4.10.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/dist/commands/audit.d.ts +49 -0
- package/dist/commands/audit.js +294 -0
- package/dist/commands/bots.js +99 -7
- package/dist/commands/skill-sync.d.ts +18 -6
- package/dist/commands/skill-sync.js +82 -7
- package/dist/database.js +1 -4
- package/dist/env-parser.d.ts +1 -1
- package/dist/env-parser.js +1 -1
- package/dist/index.js +28 -31
- package/dist/kb.d.ts +11 -0
- package/dist/kb.js +31 -0
- package/dist/slack-notify.d.ts +1 -1
- package/dist/slack-notify.js +1 -1
- package/package.json +1 -1
package/dist/commands/audit.d.ts
CHANGED
|
@@ -23,13 +23,62 @@ export interface BotAuditResult {
|
|
|
23
23
|
score: number;
|
|
24
24
|
checks: AuditCheck[];
|
|
25
25
|
}
|
|
26
|
+
interface CheckDef {
|
|
27
|
+
name: string;
|
|
28
|
+
check: (botDir: string, botId: string) => AuditCheck;
|
|
29
|
+
}
|
|
30
|
+
export interface WorkspaceStandardRow {
|
|
31
|
+
path_pattern: string;
|
|
32
|
+
entry_type: string;
|
|
33
|
+
level: string;
|
|
34
|
+
severity: string;
|
|
35
|
+
category: string;
|
|
36
|
+
bot_scope: string;
|
|
37
|
+
bot_ids: string[];
|
|
38
|
+
symlink_target: string | null;
|
|
39
|
+
content_rules: Record<string, unknown> | null;
|
|
40
|
+
description: string | null;
|
|
41
|
+
fix_action: string | null;
|
|
42
|
+
fix_template: string | null;
|
|
43
|
+
}
|
|
44
|
+
export declare function loadCheckDefs(pool: Pool): Promise<{
|
|
45
|
+
defs: CheckDef[];
|
|
46
|
+
rows: WorkspaceStandardRow[];
|
|
47
|
+
}>;
|
|
26
48
|
/** Sync version using fallback CHECK_DEFS (no DB) */
|
|
27
49
|
export declare function auditBot(botDir: string, botId: string): BotAuditResult;
|
|
28
50
|
/** Async version using DB-loaded rules */
|
|
29
51
|
export declare function auditBotFromDb(botDir: string, botId: string, pool: Pool): Promise<BotAuditResult>;
|
|
52
|
+
/** Legacy hardcoded fix — used when DB is unavailable */
|
|
30
53
|
export declare function fixBot(botDir: string, botId: string, checks: AuditCheck[]): number;
|
|
54
|
+
/**
|
|
55
|
+
* DB `fix_action`/`fix_template` 기반 auto-fix.
|
|
56
|
+
* 지원 액션: create_file, create_dir, create_symlink, delete (--force 필요)
|
|
57
|
+
*/
|
|
58
|
+
export declare function fixBotFromDb(botDir: string, botId: string, checks: AuditCheck[], dbRules: WorkspaceStandardRow[], options?: {
|
|
59
|
+
force?: boolean;
|
|
60
|
+
}): {
|
|
61
|
+
fixed: number;
|
|
62
|
+
skipped: string[];
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* DB의 required 규칙에 대해 파일이 없으면 생성.
|
|
66
|
+
* content_rules 위반은 보고만 한다.
|
|
67
|
+
*/
|
|
68
|
+
export declare function syncBotFromDb(botDir: string, botId: string, dbRules: WorkspaceStandardRow[]): {
|
|
69
|
+
created: number;
|
|
70
|
+
violations: string[];
|
|
71
|
+
};
|
|
72
|
+
export interface SkillAuditCheck extends AuditCheck {
|
|
73
|
+
skillName?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 봇의 skills/ 하위 스킬 디렉토리 구조 검증
|
|
77
|
+
*/
|
|
78
|
+
export declare function auditSkillStructure(botDir: string, botId: string): SkillAuditCheck[];
|
|
31
79
|
export declare function auditBotKb(pool: Pool): Promise<AuditCheck[]>;
|
|
32
80
|
export declare function auditBotDb(botId: string, pool: Pool): Promise<AuditCheck[]>;
|
|
33
81
|
export declare function mergeDbChecks(result: BotAuditResult, dbChecks: AuditCheck[]): BotAuditResult;
|
|
34
82
|
export declare function formatAuditSlack(results: BotAuditResult[]): string;
|
|
35
83
|
export declare function storeAuditResults(results: BotAuditResult[], client: PoolClient): Promise<void>;
|
|
84
|
+
export {};
|
package/dist/commands/audit.js
CHANGED
|
@@ -46,9 +46,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
46
46
|
};
|
|
47
47
|
})();
|
|
48
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.loadCheckDefs = loadCheckDefs;
|
|
49
50
|
exports.auditBot = auditBot;
|
|
50
51
|
exports.auditBotFromDb = auditBotFromDb;
|
|
51
52
|
exports.fixBot = fixBot;
|
|
53
|
+
exports.fixBotFromDb = fixBotFromDb;
|
|
54
|
+
exports.syncBotFromDb = syncBotFromDb;
|
|
55
|
+
exports.auditSkillStructure = auditSkillStructure;
|
|
52
56
|
exports.auditBotKb = auditBotKb;
|
|
53
57
|
exports.auditBotDb = auditBotDb;
|
|
54
58
|
exports.mergeDbChecks = mergeDbChecks;
|
|
@@ -324,6 +328,7 @@ const FIXABLE_DIRS = {
|
|
|
324
328
|
const FILE_TEMPLATES = {
|
|
325
329
|
"SOUL.md": (botId) => `# ${botId} — SOUL\n\n## Identity\n\n> TODO\n\n## R&R\n\n> TODO\n\n## KB Lookup Protocol\n\n> semo kb get/search로 팀 정보 조회\n\n## Operating Procedures\n\n> TODO\n\n## NON-NEGOTIABLE\n\n1. TODO\n`,
|
|
326
330
|
};
|
|
331
|
+
/** Legacy hardcoded fix — used when DB is unavailable */
|
|
327
332
|
function fixBot(botDir, botId, checks) {
|
|
328
333
|
let fixed = 0;
|
|
329
334
|
for (const check of checks) {
|
|
@@ -384,6 +389,295 @@ function fixBot(botDir, botId, checks) {
|
|
|
384
389
|
return fixed;
|
|
385
390
|
}
|
|
386
391
|
// ============================================================
|
|
392
|
+
// DB-based auto-fix — fix_action/fix_template 활용
|
|
393
|
+
// ============================================================
|
|
394
|
+
/**
|
|
395
|
+
* DB `fix_action`/`fix_template` 기반 auto-fix.
|
|
396
|
+
* 지원 액션: create_file, create_dir, create_symlink, delete (--force 필요)
|
|
397
|
+
*/
|
|
398
|
+
function fixBotFromDb(botDir, botId, checks, dbRules, options = {}) {
|
|
399
|
+
let fixed = 0;
|
|
400
|
+
const skipped = [];
|
|
401
|
+
const home = process.env.HOME || "/Users/reus";
|
|
402
|
+
// Build a map of check name → row for quick lookup
|
|
403
|
+
const failedCheckNames = new Set(checks.filter((c) => !c.passed).map((c) => c.name));
|
|
404
|
+
for (const row of dbRules) {
|
|
405
|
+
if (!row.fix_action || !row.fix_template)
|
|
406
|
+
continue;
|
|
407
|
+
const checkName = `${row.category}/${row.path_pattern}`;
|
|
408
|
+
// Check if any of the failed checks relate to this row
|
|
409
|
+
const isRelevant = failedCheckNames.has(checkName) ||
|
|
410
|
+
failedCheckNames.has(`${checkName}-symlink`);
|
|
411
|
+
if (!isRelevant)
|
|
412
|
+
continue;
|
|
413
|
+
// Variable substitution in fix_template
|
|
414
|
+
const resolvedTemplate = row.fix_template
|
|
415
|
+
.replace(/\$BOT_ID/g, botId)
|
|
416
|
+
.replace(/\$HOME/g, home);
|
|
417
|
+
const targetPath = path.join(botDir, row.path_pattern);
|
|
418
|
+
switch (row.fix_action) {
|
|
419
|
+
case "create_file": {
|
|
420
|
+
if (fs.existsSync(targetPath))
|
|
421
|
+
break;
|
|
422
|
+
const dir = path.dirname(targetPath);
|
|
423
|
+
if (!fs.existsSync(dir)) {
|
|
424
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
425
|
+
}
|
|
426
|
+
fs.writeFileSync(targetPath, resolvedTemplate, "utf-8");
|
|
427
|
+
fixed++;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case "create_dir": {
|
|
431
|
+
const dirPath = row.path_pattern.endsWith("/")
|
|
432
|
+
? path.join(botDir, row.path_pattern.slice(0, -1))
|
|
433
|
+
: targetPath;
|
|
434
|
+
if (!fs.existsSync(dirPath)) {
|
|
435
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
436
|
+
fixed++;
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
case "create_symlink": {
|
|
441
|
+
const symlinkTarget = resolvedTemplate;
|
|
442
|
+
if (!fs.existsSync(symlinkTarget))
|
|
443
|
+
break;
|
|
444
|
+
if (fs.existsSync(targetPath)) {
|
|
445
|
+
const stats = fs.lstatSync(targetPath);
|
|
446
|
+
if (stats.isSymbolicLink()) {
|
|
447
|
+
const existing = fs.readlinkSync(targetPath);
|
|
448
|
+
if (existing === symlinkTarget)
|
|
449
|
+
break; // already correct
|
|
450
|
+
fs.unlinkSync(targetPath);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
fs.unlinkSync(targetPath);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
fs.symlinkSync(symlinkTarget, targetPath);
|
|
457
|
+
fixed++;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "delete": {
|
|
461
|
+
if (!fs.existsSync(targetPath))
|
|
462
|
+
break;
|
|
463
|
+
if (!options.force) {
|
|
464
|
+
skipped.push(`${row.path_pattern} (delete requires --force)`);
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
const stats = fs.lstatSync(targetPath);
|
|
468
|
+
if (stats.isDirectory()) {
|
|
469
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
fs.unlinkSync(targetPath);
|
|
473
|
+
}
|
|
474
|
+
fixed++;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return { fixed, skipped };
|
|
480
|
+
}
|
|
481
|
+
// ============================================================
|
|
482
|
+
// --sync: DB required 항목 proactive 보장
|
|
483
|
+
// ============================================================
|
|
484
|
+
/**
|
|
485
|
+
* DB의 required 규칙에 대해 파일이 없으면 생성.
|
|
486
|
+
* content_rules 위반은 보고만 한다.
|
|
487
|
+
*/
|
|
488
|
+
function syncBotFromDb(botDir, botId, dbRules) {
|
|
489
|
+
let created = 0;
|
|
490
|
+
const violations = [];
|
|
491
|
+
const home = process.env.HOME || "/Users/reus";
|
|
492
|
+
for (const row of dbRules) {
|
|
493
|
+
if (row.level !== "required")
|
|
494
|
+
continue;
|
|
495
|
+
if (row.path_pattern.includes("*"))
|
|
496
|
+
continue; // glob patterns handled separately
|
|
497
|
+
const targetPath = path.join(botDir, row.path_pattern);
|
|
498
|
+
if (row.entry_type === "dir") {
|
|
499
|
+
const dirPath = row.path_pattern.endsWith("/")
|
|
500
|
+
? path.join(botDir, row.path_pattern.slice(0, -1))
|
|
501
|
+
: targetPath;
|
|
502
|
+
if (!fs.existsSync(dirPath)) {
|
|
503
|
+
if (row.fix_action === "create_dir") {
|
|
504
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
505
|
+
created++;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else if (row.entry_type === "symlink") {
|
|
510
|
+
if (!fs.existsSync(targetPath) && row.fix_action === "create_symlink" && row.fix_template) {
|
|
511
|
+
const symlinkTarget = row.fix_template
|
|
512
|
+
.replace(/\$BOT_ID/g, botId)
|
|
513
|
+
.replace(/\$HOME/g, home);
|
|
514
|
+
if (fs.existsSync(symlinkTarget)) {
|
|
515
|
+
const dir = path.dirname(targetPath);
|
|
516
|
+
if (!fs.existsSync(dir))
|
|
517
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
518
|
+
fs.symlinkSync(symlinkTarget, targetPath);
|
|
519
|
+
created++;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
// file
|
|
525
|
+
if (!fs.existsSync(targetPath)) {
|
|
526
|
+
if (row.fix_action === "create_file" && row.fix_template) {
|
|
527
|
+
const content = row.fix_template
|
|
528
|
+
.replace(/\$BOT_ID/g, botId)
|
|
529
|
+
.replace(/\$HOME/g, home);
|
|
530
|
+
const dir = path.dirname(targetPath);
|
|
531
|
+
if (!fs.existsSync(dir))
|
|
532
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
533
|
+
fs.writeFileSync(targetPath, content, "utf-8");
|
|
534
|
+
created++;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else if (row.content_rules) {
|
|
538
|
+
// File exists — check content_rules violations (report only)
|
|
539
|
+
const rules = row.content_rules;
|
|
540
|
+
const maxLines = rules.max_lines;
|
|
541
|
+
if (maxLines) {
|
|
542
|
+
try {
|
|
543
|
+
const lines = fs.readFileSync(targetPath, "utf-8").split("\n").length;
|
|
544
|
+
if (lines > maxLines) {
|
|
545
|
+
violations.push(`${row.path_pattern}: ${lines} lines (max ${maxLines})`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch { /* skip unreadable */ }
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { created, violations };
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* 봇의 skills/ 하위 스킬 디렉토리 구조 검증
|
|
557
|
+
*/
|
|
558
|
+
function auditSkillStructure(botDir, botId) {
|
|
559
|
+
const checks = [];
|
|
560
|
+
const skillsDir = path.join(botDir, "skills");
|
|
561
|
+
if (!fs.existsSync(skillsDir) || !fs.statSync(skillsDir).isDirectory()) {
|
|
562
|
+
return checks;
|
|
563
|
+
}
|
|
564
|
+
let entries;
|
|
565
|
+
try {
|
|
566
|
+
entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return checks;
|
|
570
|
+
}
|
|
571
|
+
for (const entry of entries) {
|
|
572
|
+
if (!entry.isDirectory())
|
|
573
|
+
continue;
|
|
574
|
+
if (entry.name.startsWith("_") || entry.name.startsWith("."))
|
|
575
|
+
continue;
|
|
576
|
+
if (entry.name.endsWith(".skill"))
|
|
577
|
+
continue;
|
|
578
|
+
const skillName = entry.name;
|
|
579
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
580
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
581
|
+
// SKILL.md 존재 여부
|
|
582
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
583
|
+
checks.push({
|
|
584
|
+
name: `skill/${skillName}/SKILL.md`,
|
|
585
|
+
passed: false,
|
|
586
|
+
detail: `skills/${skillName}/SKILL.md missing`,
|
|
587
|
+
skillName,
|
|
588
|
+
});
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
// SKILL.md 내용 검증
|
|
592
|
+
let content;
|
|
593
|
+
try {
|
|
594
|
+
content = fs.readFileSync(skillMdPath, "utf-8");
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
checks.push({
|
|
598
|
+
name: `skill/${skillName}/SKILL.md`,
|
|
599
|
+
passed: false,
|
|
600
|
+
detail: `skills/${skillName}/SKILL.md unreadable`,
|
|
601
|
+
skillName,
|
|
602
|
+
});
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
// SKILL.md exists
|
|
606
|
+
checks.push({
|
|
607
|
+
name: `skill/${skillName}/SKILL.md`,
|
|
608
|
+
passed: true,
|
|
609
|
+
detail: `skills/${skillName}/SKILL.md exists`,
|
|
610
|
+
skillName,
|
|
611
|
+
});
|
|
612
|
+
// Line count check (max 500)
|
|
613
|
+
const lines = content.split("\n").length;
|
|
614
|
+
checks.push({
|
|
615
|
+
name: `skill/${skillName}/lines`,
|
|
616
|
+
passed: lines <= 500,
|
|
617
|
+
detail: lines <= 500
|
|
618
|
+
? `skills/${skillName}/SKILL.md: ${lines} lines (≤ 500)`
|
|
619
|
+
: `skills/${skillName}/SKILL.md: ${lines} lines (> 500, too long)`,
|
|
620
|
+
skillName,
|
|
621
|
+
});
|
|
622
|
+
// Frontmatter check (---\nname: ...\ndescription: ...\n---)
|
|
623
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
624
|
+
if (fmMatch) {
|
|
625
|
+
const fm = fmMatch[1];
|
|
626
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
627
|
+
const descMatch = fm.match(/^description:/m);
|
|
628
|
+
if (nameMatch) {
|
|
629
|
+
const fmName = nameMatch[1].trim();
|
|
630
|
+
const isKebab = /^[a-z0-9]+(-[a-z0-9]+)*$/.test(fmName);
|
|
631
|
+
checks.push({
|
|
632
|
+
name: `skill/${skillName}/name-kebab`,
|
|
633
|
+
passed: isKebab,
|
|
634
|
+
detail: isKebab
|
|
635
|
+
? `skills/${skillName} name "${fmName}" is kebab-case`
|
|
636
|
+
: `skills/${skillName} name "${fmName}" is not kebab-case`,
|
|
637
|
+
skillName,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
checks.push({
|
|
642
|
+
name: `skill/${skillName}/name-kebab`,
|
|
643
|
+
passed: false,
|
|
644
|
+
detail: `skills/${skillName}/SKILL.md frontmatter missing 'name'`,
|
|
645
|
+
skillName,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
checks.push({
|
|
649
|
+
name: `skill/${skillName}/description`,
|
|
650
|
+
passed: !!descMatch,
|
|
651
|
+
detail: descMatch
|
|
652
|
+
? `skills/${skillName}/SKILL.md has description`
|
|
653
|
+
: `skills/${skillName}/SKILL.md frontmatter missing 'description'`,
|
|
654
|
+
skillName,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
checks.push({
|
|
659
|
+
name: `skill/${skillName}/frontmatter`,
|
|
660
|
+
passed: false,
|
|
661
|
+
detail: `skills/${skillName}/SKILL.md missing frontmatter (--- block)`,
|
|
662
|
+
skillName,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
// Forbidden files check
|
|
666
|
+
const forbiddenFiles = ["README.md", "CHANGELOG.md", "INSTALL.md", "LICENSE", "LICENSE.md"];
|
|
667
|
+
for (const forbidden of forbiddenFiles) {
|
|
668
|
+
if (fs.existsSync(path.join(skillDir, forbidden))) {
|
|
669
|
+
checks.push({
|
|
670
|
+
name: `skill/${skillName}/forbidden-${forbidden}`,
|
|
671
|
+
passed: false,
|
|
672
|
+
detail: `skills/${skillName}/${forbidden} exists (forbidden)`,
|
|
673
|
+
skillName,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return checks;
|
|
679
|
+
}
|
|
680
|
+
// ============================================================
|
|
387
681
|
// KB domain checks (async, requires pool)
|
|
388
682
|
// ============================================================
|
|
389
683
|
const KB_REQUIRED_DOMAINS = ["semicolon"];
|
package/dist/commands/bots.js
CHANGED
|
@@ -561,7 +561,9 @@ function registerBotsCommands(program) {
|
|
|
561
561
|
.command("audit")
|
|
562
562
|
.description("봇 워크스페이스 표준 구조 audit")
|
|
563
563
|
.option("--format <type>", "출력 형식 (table|json|slack)", "table")
|
|
564
|
-
.option("--fix", "누락 파일/디렉토리 자동 생성")
|
|
564
|
+
.option("--fix", "누락 파일/디렉토리 자동 생성 (DB fix_action 활용)")
|
|
565
|
+
.option("--sync", "DB required 항목 proactive 보장 (누락 파일 생성)")
|
|
566
|
+
.option("--force", "delete fix_action 실행 허용 (--fix와 함께 사용)")
|
|
565
567
|
.option("--no-db", "DB 저장 건너뛰기")
|
|
566
568
|
.option("--local", "~/.claude/semo/bots/ 로컬 미러 audit")
|
|
567
569
|
.action(async (options) => {
|
|
@@ -596,14 +598,74 @@ function registerBotsCommands(program) {
|
|
|
596
598
|
// Run audit — try DB-based rules first, fallback to hardcoded
|
|
597
599
|
let results;
|
|
598
600
|
const dbConnected = await (0, database_1.isDbConnected)();
|
|
601
|
+
let dbRules = [];
|
|
599
602
|
if (dbConnected) {
|
|
600
603
|
const pool = (0, database_1.getPool)();
|
|
604
|
+
try {
|
|
605
|
+
const { rows } = await (0, audit_1.loadCheckDefs)(pool);
|
|
606
|
+
dbRules = rows;
|
|
607
|
+
}
|
|
608
|
+
catch { /* DB rules load failed, will use fallback */ }
|
|
601
609
|
results = await Promise.all(botEntries.map(({ botId, botDir }) => (0, audit_1.auditBotFromDb)(botDir, botId, pool)));
|
|
602
610
|
}
|
|
603
611
|
else {
|
|
604
612
|
results = botEntries.map(({ botId, botDir }) => (0, audit_1.auditBot)(botDir, botId));
|
|
605
613
|
}
|
|
614
|
+
// Merge skill structure checks into results
|
|
615
|
+
for (let i = 0; i < results.length; i++) {
|
|
616
|
+
const skillChecks = (0, audit_1.auditSkillStructure)(botEntries[i].botDir, botEntries[i].botId);
|
|
617
|
+
if (skillChecks.length > 0) {
|
|
618
|
+
results[i] = (0, audit_1.mergeDbChecks)(results[i], skillChecks);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
606
621
|
spinner.stop();
|
|
622
|
+
// --sync: DB required 항목 proactive 보장
|
|
623
|
+
if (options.sync && dbRules.length > 0) {
|
|
624
|
+
let totalCreated = 0;
|
|
625
|
+
const allViolations = [];
|
|
626
|
+
for (const { botId, botDir } of botEntries) {
|
|
627
|
+
const botSpecificRules = dbRules.filter((row) => {
|
|
628
|
+
if (row.bot_scope === "all")
|
|
629
|
+
return true;
|
|
630
|
+
if (row.bot_scope === "include")
|
|
631
|
+
return row.bot_ids.includes(botId);
|
|
632
|
+
if (row.bot_scope === "exclude")
|
|
633
|
+
return !row.bot_ids.includes(botId);
|
|
634
|
+
return true;
|
|
635
|
+
});
|
|
636
|
+
const { created, violations } = (0, audit_1.syncBotFromDb)(botDir, botId, botSpecificRules);
|
|
637
|
+
if (created > 0) {
|
|
638
|
+
console.log(chalk_1.default.green(` ✔ ${botId}: ${created}개 항목 동기화 생성`));
|
|
639
|
+
totalCreated += created;
|
|
640
|
+
}
|
|
641
|
+
allViolations.push(...violations.map((v) => `${botId}: ${v}`));
|
|
642
|
+
}
|
|
643
|
+
if (totalCreated > 0) {
|
|
644
|
+
console.log(chalk_1.default.green(`\n총 ${totalCreated}개 동기화`));
|
|
645
|
+
}
|
|
646
|
+
if (allViolations.length > 0) {
|
|
647
|
+
console.log(chalk_1.default.yellow(`\n⚠ content_rules 위반 (보고만):`));
|
|
648
|
+
for (const v of allViolations) {
|
|
649
|
+
console.log(chalk_1.default.yellow(` - ${v}`));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Re-audit after sync
|
|
653
|
+
if (totalCreated > 0) {
|
|
654
|
+
if (dbConnected) {
|
|
655
|
+
const pool = (0, database_1.getPool)();
|
|
656
|
+
results = await Promise.all(botEntries.map(({ botId, botDir }) => (0, audit_1.auditBotFromDb)(botDir, botId, pool)));
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
results = botEntries.map(({ botId, botDir }) => (0, audit_1.auditBot)(botDir, botId));
|
|
660
|
+
}
|
|
661
|
+
for (let i = 0; i < results.length; i++) {
|
|
662
|
+
const skillChecks = (0, audit_1.auditSkillStructure)(botEntries[i].botDir, botEntries[i].botId);
|
|
663
|
+
if (skillChecks.length > 0) {
|
|
664
|
+
results[i] = (0, audit_1.mergeDbChecks)(results[i], skillChecks);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
607
669
|
// --fix
|
|
608
670
|
if (options.fix) {
|
|
609
671
|
let totalFixed = 0;
|
|
@@ -611,20 +673,50 @@ function registerBotsCommands(program) {
|
|
|
611
673
|
const botDir = isLocal
|
|
612
674
|
? path.join(home, ".claude", "semo", "bots", r.botId)
|
|
613
675
|
: path.join(home, `.openclaw-${r.botId}`, "workspace");
|
|
614
|
-
|
|
676
|
+
let fixed;
|
|
677
|
+
if (dbRules.length > 0) {
|
|
678
|
+
// DB-based fix
|
|
679
|
+
const botSpecificRules = dbRules.filter((row) => {
|
|
680
|
+
if (row.bot_scope === "all")
|
|
681
|
+
return true;
|
|
682
|
+
if (row.bot_scope === "include")
|
|
683
|
+
return row.bot_ids.includes(r.botId);
|
|
684
|
+
if (row.bot_scope === "exclude")
|
|
685
|
+
return !row.bot_ids.includes(r.botId);
|
|
686
|
+
return true;
|
|
687
|
+
});
|
|
688
|
+
const result = (0, audit_1.fixBotFromDb)(botDir, r.botId, r.checks, botSpecificRules, { force: options.force });
|
|
689
|
+
fixed = result.fixed;
|
|
690
|
+
if (result.skipped.length > 0) {
|
|
691
|
+
for (const s of result.skipped) {
|
|
692
|
+
console.log(chalk_1.default.yellow(` ⚠ ${r.botId}: ${s}`));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
// Fallback to hardcoded fix
|
|
698
|
+
fixed = (0, audit_1.fixBot)(botDir, r.botId, r.checks);
|
|
699
|
+
}
|
|
615
700
|
if (fixed > 0) {
|
|
616
|
-
console.log(chalk_1.default.green(` ✔ ${r.botId}: ${fixed}개 파일/디렉토리
|
|
701
|
+
console.log(chalk_1.default.green(` ✔ ${r.botId}: ${fixed}개 파일/디렉토리 수정`));
|
|
617
702
|
totalFixed += fixed;
|
|
618
703
|
}
|
|
619
704
|
}
|
|
620
705
|
if (totalFixed > 0) {
|
|
621
706
|
console.log(chalk_1.default.green(`\n총 ${totalFixed}개 수정`));
|
|
622
707
|
// Re-audit after fix
|
|
708
|
+
if (dbConnected) {
|
|
709
|
+
const pool = (0, database_1.getPool)();
|
|
710
|
+
results = await Promise.all(botEntries.map(({ botId, botDir }) => (0, audit_1.auditBotFromDb)(botDir, botId, pool)));
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
results = botEntries.map(({ botId, botDir }) => (0, audit_1.auditBot)(botDir, botId));
|
|
714
|
+
}
|
|
623
715
|
for (let i = 0; i < results.length; i++) {
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
716
|
+
const skillChecks = (0, audit_1.auditSkillStructure)(botEntries[i].botDir, botEntries[i].botId);
|
|
717
|
+
if (skillChecks.length > 0) {
|
|
718
|
+
results[i] = (0, audit_1.mergeDbChecks)(results[i], skillChecks);
|
|
719
|
+
}
|
|
628
720
|
}
|
|
629
721
|
}
|
|
630
722
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* skill-sync — 봇
|
|
2
|
+
* skill-sync — 봇 워크스페이스 스킬 스캔 + DB 동기화
|
|
3
|
+
*
|
|
4
|
+
* v2.0: ~/.openclaw-{bot}/workspace/skills/ 경로 스캔 (semo-system/ 폐기)
|
|
5
|
+
* 봇 목록은 bot_status DB에서 동적 로드, fallback으로 로컬 디렉토리 스캔.
|
|
3
6
|
*
|
|
4
7
|
* semo bots sync (piggyback) 및 semo context sync (세션 훅) 양쪽에서 호출.
|
|
5
8
|
* 스킬 이름은 flat (예: 'kb-manager'), metadata.bot_ids 배열로 봇 매핑.
|
|
6
9
|
* 동일 스킬명이 여러 봇에 존재하면 bot_ids를 머지.
|
|
7
10
|
*/
|
|
8
|
-
import { PoolClient } from "pg";
|
|
9
|
-
interface ScannedSkill {
|
|
11
|
+
import { Pool, PoolClient } from "pg";
|
|
12
|
+
export interface ScannedSkill {
|
|
10
13
|
name: string;
|
|
11
14
|
prompt: string;
|
|
12
15
|
package: string;
|
|
@@ -17,12 +20,21 @@ export interface SkillSyncResult {
|
|
|
17
20
|
total: number;
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
|
-
*
|
|
23
|
+
* ~/.openclaw-{bot}/workspace/skills/ 에서 봇 전용 스킬 파일 스캔
|
|
24
|
+
*
|
|
25
|
+
* @param botIds - 스캔 대상 봇 ID 목록
|
|
26
|
+
*/
|
|
27
|
+
export declare function scanSkills(botIds: string[]): ScannedSkill[];
|
|
28
|
+
/**
|
|
29
|
+
* @deprecated semo-system/ 기반 스캔 (하위호환 유지)
|
|
21
30
|
*/
|
|
22
31
|
export declare function scanSkills(semoSystemDir: string): ScannedSkill[];
|
|
32
|
+
/**
|
|
33
|
+
* DB에서 봇 목록을 로드, fallback으로 로컬 디렉토리 스캔
|
|
34
|
+
*/
|
|
35
|
+
export declare function getBotIds(pool: Pool): Promise<string[]>;
|
|
23
36
|
/**
|
|
24
37
|
* 스캔된 스킬을 skill_definitions에 upsert
|
|
25
38
|
* flat name + metadata.bot_ids 배열 사용, 동일 스킬명은 bot_ids 머지
|
|
26
39
|
*/
|
|
27
|
-
export declare function syncSkillsToDB(client: PoolClient,
|
|
28
|
-
export {};
|
|
40
|
+
export declare function syncSkillsToDB(client: PoolClient, pool: Pool): Promise<SkillSyncResult>;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* skill-sync — 봇
|
|
3
|
+
* skill-sync — 봇 워크스페이스 스킬 스캔 + DB 동기화
|
|
4
|
+
*
|
|
5
|
+
* v2.0: ~/.openclaw-{bot}/workspace/skills/ 경로 스캔 (semo-system/ 폐기)
|
|
6
|
+
* 봇 목록은 bot_status DB에서 동적 로드, fallback으로 로컬 디렉토리 스캔.
|
|
4
7
|
*
|
|
5
8
|
* semo bots sync (piggyback) 및 semo context sync (세션 훅) 양쪽에서 호출.
|
|
6
9
|
* 스킬 이름은 flat (예: 'kb-manager'), metadata.bot_ids 배열로 봇 매핑.
|
|
@@ -41,13 +44,55 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
41
44
|
})();
|
|
42
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
46
|
exports.scanSkills = scanSkills;
|
|
47
|
+
exports.getBotIds = getBotIds;
|
|
44
48
|
exports.syncSkillsToDB = syncSkillsToDB;
|
|
45
49
|
const fs = __importStar(require("fs"));
|
|
46
50
|
const path = __importStar(require("path"));
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
function scanSkills(arg) {
|
|
52
|
+
if (typeof arg === "string") {
|
|
53
|
+
return scanSkillsLegacy(arg);
|
|
54
|
+
}
|
|
55
|
+
return scanSkillsV2(arg);
|
|
56
|
+
}
|
|
57
|
+
function scanSkillsV2(botIds) {
|
|
58
|
+
const skills = [];
|
|
59
|
+
const home = process.env.HOME || "/Users/reus";
|
|
60
|
+
for (const botId of botIds) {
|
|
61
|
+
const skillsDir = path.join(home, `.openclaw-${botId}`, "workspace", "skills");
|
|
62
|
+
if (!fs.existsSync(skillsDir))
|
|
63
|
+
continue;
|
|
64
|
+
let skillEntries;
|
|
65
|
+
try {
|
|
66
|
+
skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const skillEntry of skillEntries) {
|
|
72
|
+
if (!skillEntry.isDirectory())
|
|
73
|
+
continue;
|
|
74
|
+
if (skillEntry.name.endsWith(".skill"))
|
|
75
|
+
continue;
|
|
76
|
+
if (skillEntry.name.startsWith("_") || skillEntry.name.startsWith("."))
|
|
77
|
+
continue;
|
|
78
|
+
const skillMdPath = path.join(skillsDir, skillEntry.name, "SKILL.md");
|
|
79
|
+
if (!fs.existsSync(skillMdPath))
|
|
80
|
+
continue;
|
|
81
|
+
try {
|
|
82
|
+
skills.push({
|
|
83
|
+
name: skillEntry.name,
|
|
84
|
+
prompt: fs.readFileSync(skillMdPath, "utf-8"),
|
|
85
|
+
package: "openclaw",
|
|
86
|
+
botId,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch { /* skip unreadable */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return skills;
|
|
93
|
+
}
|
|
94
|
+
/** Legacy: semo-system/bot-workspaces 에서 스캔 (하위호환) */
|
|
95
|
+
function scanSkillsLegacy(semoSystemDir) {
|
|
51
96
|
const skills = [];
|
|
52
97
|
const workspacesDir = path.join(semoSystemDir, "bot-workspaces");
|
|
53
98
|
if (!fs.existsSync(workspacesDir))
|
|
@@ -81,12 +126,42 @@ function scanSkills(semoSystemDir) {
|
|
|
81
126
|
}
|
|
82
127
|
return skills;
|
|
83
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* DB에서 봇 목록을 로드, fallback으로 로컬 디렉토리 스캔
|
|
131
|
+
*/
|
|
132
|
+
async function getBotIds(pool) {
|
|
133
|
+
try {
|
|
134
|
+
const result = await pool.query("SELECT bot_id FROM semo.bot_status ORDER BY bot_id");
|
|
135
|
+
if (result.rows.length > 0) {
|
|
136
|
+
return result.rows.map((r) => r.bot_id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch { /* fallback to local scan */ }
|
|
140
|
+
// Fallback: scan ~/.openclaw-*/workspace/ directories
|
|
141
|
+
const home = process.env.HOME || "/Users/reus";
|
|
142
|
+
const botIds = [];
|
|
143
|
+
try {
|
|
144
|
+
const entries = fs.readdirSync(home);
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
const match = entry.match(/^\.openclaw-(.+)$/);
|
|
147
|
+
if (match) {
|
|
148
|
+
const wsDir = path.join(home, entry, "workspace");
|
|
149
|
+
if (fs.existsSync(wsDir)) {
|
|
150
|
+
botIds.push(match[1]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch { /* skip */ }
|
|
156
|
+
return botIds.sort();
|
|
157
|
+
}
|
|
84
158
|
/**
|
|
85
159
|
* 스캔된 스킬을 skill_definitions에 upsert
|
|
86
160
|
* flat name + metadata.bot_ids 배열 사용, 동일 스킬명은 bot_ids 머지
|
|
87
161
|
*/
|
|
88
|
-
async function syncSkillsToDB(client,
|
|
89
|
-
const
|
|
162
|
+
async function syncSkillsToDB(client, pool) {
|
|
163
|
+
const botIds = await getBotIds(pool);
|
|
164
|
+
const skills = scanSkills(botIds);
|
|
90
165
|
for (const skill of skills) {
|
|
91
166
|
await client.query(`INSERT INTO semo.skill_definitions (name, prompt, package, metadata, is_active, office_id)
|
|
92
167
|
VALUES ($1, $2, $3, $4, true, NULL)
|
package/dist/database.js
CHANGED
|
@@ -68,11 +68,8 @@ const env_parser_1 = require("./env-parser");
|
|
|
68
68
|
// ~/.claude/semo/.env 자동 로드 — LaunchAgent / Claude Code 앱 / cron 등
|
|
69
69
|
// 인터랙티브 쉘이 아닌 환경에서 환경변수를 공급한다.
|
|
70
70
|
// 이미 설정된 환경변수는 덮어쓰지 않는다 (env var > file).
|
|
71
|
-
// v4.5.0: ~/.semo.env → ~/.claude/semo/.env 이전. 구 경로 폴백 유지.
|
|
72
71
|
function loadSemoEnv() {
|
|
73
|
-
const
|
|
74
|
-
const legacyEnvFile = path.join(os.homedir(), ".semo.env");
|
|
75
|
-
const envFile = fs.existsSync(newEnvFile) ? newEnvFile : legacyEnvFile;
|
|
72
|
+
const envFile = path.join(os.homedir(), ".claude", "semo", ".env");
|
|
76
73
|
if (!fs.existsSync(envFile))
|
|
77
74
|
return;
|
|
78
75
|
try {
|
package/dist/env-parser.d.ts
CHANGED
package/dist/env-parser.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parseEnvContent = parseEnvContent;
|
|
4
4
|
/**
|
|
5
5
|
* 공용 KEY=VALUE 파서
|
|
6
|
-
* ~/.semo
|
|
6
|
+
* ~/.claude/semo/.env 파일과 GitHub Gist 콘텐츠 모두 이 함수로 파싱한다.
|
|
7
7
|
*/
|
|
8
8
|
function parseEnvContent(content) {
|
|
9
9
|
const result = {};
|
package/dist/index.js
CHANGED
|
@@ -331,8 +331,7 @@ async function showToolsStatus() {
|
|
|
331
331
|
// === 글로벌 설정 체크 ===
|
|
332
332
|
function isGlobalSetupDone() {
|
|
333
333
|
const home = os.homedir();
|
|
334
|
-
const hasEnv = fs.existsSync(path.join(home, ".claude", "semo", ".env"))
|
|
335
|
-
fs.existsSync(path.join(home, ".semo.env")); // 하위 호환
|
|
334
|
+
const hasEnv = fs.existsSync(path.join(home, ".claude", "semo", ".env"));
|
|
336
335
|
const hasSetup = fs.existsSync(path.join(home, ".claude", "semo", "SOUL.md")) ||
|
|
337
336
|
fs.existsSync(path.join(home, ".claude", "skills")); // 하위 호환
|
|
338
337
|
return hasEnv && hasSetup;
|
|
@@ -494,9 +493,7 @@ const BASE_MCP_SERVERS = [
|
|
|
494
493
|
},
|
|
495
494
|
];
|
|
496
495
|
// === ~/.claude/semo/.env 설정 (자동 감지 → Gist → 프롬프트) ===
|
|
497
|
-
// v4.5.0: ~/.semo.env → ~/.claude/semo/.env 이전
|
|
498
496
|
const SEMO_ENV_PATH = path.join(os.homedir(), ".claude", "semo", ".env");
|
|
499
|
-
const LEGACY_ENV_PATH = path.join(os.homedir(), ".semo.env");
|
|
500
497
|
const SEMO_CREDENTIALS = [
|
|
501
498
|
{
|
|
502
499
|
key: "DATABASE_URL",
|
|
@@ -544,27 +541,9 @@ function writeSemoEnvFile(creds) {
|
|
|
544
541
|
}
|
|
545
542
|
lines.push("");
|
|
546
543
|
fs.writeFileSync(SEMO_ENV_PATH, lines.join("\n"), { mode: 0o600 });
|
|
547
|
-
// 하위 호환 심링크: ~/.semo.env → ~/.claude/semo/.env
|
|
548
|
-
try {
|
|
549
|
-
if (fs.existsSync(LEGACY_ENV_PATH)) {
|
|
550
|
-
const stat = fs.lstatSync(LEGACY_ENV_PATH);
|
|
551
|
-
if (!stat.isSymbolicLink()) {
|
|
552
|
-
// 기존 실파일은 백업 후 심링크로 교체
|
|
553
|
-
fs.renameSync(LEGACY_ENV_PATH, LEGACY_ENV_PATH + ".bak");
|
|
554
|
-
}
|
|
555
|
-
else {
|
|
556
|
-
fs.unlinkSync(LEGACY_ENV_PATH);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
fs.symlinkSync(SEMO_ENV_PATH, LEGACY_ENV_PATH);
|
|
560
|
-
}
|
|
561
|
-
catch {
|
|
562
|
-
// 심링크 실패 시 무시 — 새 경로가 원본
|
|
563
|
-
}
|
|
564
544
|
}
|
|
565
545
|
function readSemoEnvCreds() {
|
|
566
|
-
|
|
567
|
-
const envFile = fs.existsSync(SEMO_ENV_PATH) ? SEMO_ENV_PATH : LEGACY_ENV_PATH;
|
|
546
|
+
const envFile = SEMO_ENV_PATH;
|
|
568
547
|
if (!fs.existsSync(envFile))
|
|
569
548
|
return {};
|
|
570
549
|
try {
|
|
@@ -740,7 +719,7 @@ async function buildKbFirstBlock() {
|
|
|
740
719
|
const label = s.desc || s.scheme_key;
|
|
741
720
|
domainGuide += `- 서비스 ${label} → \`domain: {서비스명}\`, key: \`${s.scheme_key}\`\n`;
|
|
742
721
|
}
|
|
743
|
-
domainGuide += `- 서비스 KPI → \`domain: {서비스명}\`, key: \`kpi/
|
|
722
|
+
domainGuide += `- 서비스 KPI → \`domain: {서비스명}\`, key: \`kpi/{YYYY-MM-DD}\` (최신: 가장 최근 날짜)\n`;
|
|
744
723
|
domainGuide += `- 서비스 마일스톤 → \`domain: {서비스명}\`, key: \`milestone/{slug}\`\n`;
|
|
745
724
|
domainGuide += `\n**정확한 경로를 모를 때:**\n`;
|
|
746
725
|
domainGuide += `1. \`semo kb search "검색어"\` → 결과의 \`[domain] key/sub_key\` 경로 확인\n`;
|
|
@@ -853,7 +832,7 @@ async function setupHooks(isUpdate = false) {
|
|
|
853
832
|
hooks: [
|
|
854
833
|
{
|
|
855
834
|
type: "command",
|
|
856
|
-
command: ". ~/.claude/semo/.env 2>/dev/null
|
|
835
|
+
command: ". ~/.claude/semo/.env 2>/dev/null; semo context sync 2>/dev/null || true",
|
|
857
836
|
timeout: 30,
|
|
858
837
|
},
|
|
859
838
|
],
|
|
@@ -865,7 +844,7 @@ async function setupHooks(isUpdate = false) {
|
|
|
865
844
|
hooks: [
|
|
866
845
|
{
|
|
867
846
|
type: "command",
|
|
868
|
-
command: ". ~/.claude/semo/.env 2>/dev/null
|
|
847
|
+
command: ". ~/.claude/semo/.env 2>/dev/null; semo context push 2>/dev/null || true",
|
|
869
848
|
timeout: 30,
|
|
870
849
|
},
|
|
871
850
|
],
|
|
@@ -1119,9 +1098,9 @@ npm run build # 빌드 검증
|
|
|
1119
1098
|
|
|
1120
1099
|
---
|
|
1121
1100
|
|
|
1122
|
-
## 환경변수 (\`~/.semo
|
|
1101
|
+
## 환경변수 (\`~/.claude/semo/.env\`)
|
|
1123
1102
|
|
|
1124
|
-
SEMO는 \`~/.semo
|
|
1103
|
+
SEMO는 \`~/.claude/semo/.env\` 파일에서 팀 공통 환경변수를 로드합니다.
|
|
1125
1104
|
SessionStart 훅과 OpenClaw 게이트웨이 래퍼에서 자동 source됩니다.
|
|
1126
1105
|
|
|
1127
1106
|
| 변수 | 용도 | 필수 |
|
|
@@ -1130,7 +1109,7 @@ SessionStart 훅과 OpenClaw 게이트웨이 래퍼에서 자동 source됩니다
|
|
|
1130
1109
|
| \`OPENAI_API_KEY\` | KB 임베딩용 (text-embedding-3-small) | 선택 |
|
|
1131
1110
|
| \`SLACK_WEBHOOK\` | Slack 알림 | 선택 |
|
|
1132
1111
|
|
|
1133
|
-
키 갱신이 필요하면 \`~/.semo
|
|
1112
|
+
키 갱신이 필요하면 \`~/.claude/semo/.env\`를 직접 편집하거나 \`semo onboarding -f\`를 실행하세요.
|
|
1134
1113
|
|
|
1135
1114
|
---
|
|
1136
1115
|
|
|
@@ -1754,7 +1733,7 @@ kbCmd
|
|
|
1754
1733
|
kbCmd
|
|
1755
1734
|
.command("ontology")
|
|
1756
1735
|
.description("온톨로지 조회 — 도메인/타입/스키마/라우팅 테이블")
|
|
1757
|
-
.option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register|add-key|remove-key)", "list")
|
|
1736
|
+
.option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register|create-type|add-key|remove-key)", "list")
|
|
1758
1737
|
.option("--domain <name>", "action=show|register 시 도메인")
|
|
1759
1738
|
.option("--type <name>", "action=schema|register|add-key|remove-key 시 타입 키")
|
|
1760
1739
|
.option("--key <name>", "action=add-key|remove-key 시 스키마 키")
|
|
@@ -1949,6 +1928,24 @@ kbCmd
|
|
|
1949
1928
|
}
|
|
1950
1929
|
}
|
|
1951
1930
|
}
|
|
1931
|
+
else if (action === "create-type") {
|
|
1932
|
+
if (!options.type) {
|
|
1933
|
+
console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type project)"));
|
|
1934
|
+
process.exit(1);
|
|
1935
|
+
}
|
|
1936
|
+
const result = await (0, kb_1.ontoCreateType)(pool, {
|
|
1937
|
+
type_key: options.type,
|
|
1938
|
+
description: options.description,
|
|
1939
|
+
});
|
|
1940
|
+
if (result.success) {
|
|
1941
|
+
console.log(chalk_1.default.green(`\n✅ 온톨로지 타입 '${options.type}' 생성 완료`));
|
|
1942
|
+
console.log(chalk_1.default.gray(`\n 다음 단계: semo kb ontology --action add-key --type ${options.type} --key <key> --key-type singleton|collection\n`));
|
|
1943
|
+
}
|
|
1944
|
+
else {
|
|
1945
|
+
console.log(chalk_1.default.red(`\n❌ 타입 생성 실패: ${result.error}\n`));
|
|
1946
|
+
process.exit(1);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1952
1949
|
else if (action === "add-key") {
|
|
1953
1950
|
if (!options.type) {
|
|
1954
1951
|
console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type service)"));
|
|
@@ -1993,7 +1990,7 @@ kbCmd
|
|
|
1993
1990
|
}
|
|
1994
1991
|
}
|
|
1995
1992
|
else {
|
|
1996
|
-
console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table, register, add-key, remove-key`));
|
|
1993
|
+
console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table, register, create-type, add-key, remove-key`));
|
|
1997
1994
|
process.exit(1);
|
|
1998
1995
|
}
|
|
1999
1996
|
await (0, database_1.closeConnection)();
|
package/dist/kb.d.ts
CHANGED
|
@@ -246,6 +246,17 @@ export declare function ontoAddKey(pool: Pool, opts: {
|
|
|
246
246
|
success: boolean;
|
|
247
247
|
error?: string;
|
|
248
248
|
}>;
|
|
249
|
+
/**
|
|
250
|
+
* Create a new ontology type
|
|
251
|
+
*/
|
|
252
|
+
export declare function ontoCreateType(pool: Pool, opts: {
|
|
253
|
+
type_key: string;
|
|
254
|
+
description?: string;
|
|
255
|
+
schema?: Record<string, unknown>;
|
|
256
|
+
}): Promise<{
|
|
257
|
+
success: boolean;
|
|
258
|
+
error?: string;
|
|
259
|
+
}>;
|
|
249
260
|
/**
|
|
250
261
|
* Remove a key from a type schema
|
|
251
262
|
*/
|
package/dist/kb.js
CHANGED
|
@@ -63,6 +63,7 @@ exports.ontoListServices = ontoListServices;
|
|
|
63
63
|
exports.ontoListInstances = ontoListInstances;
|
|
64
64
|
exports.ontoRegister = ontoRegister;
|
|
65
65
|
exports.ontoAddKey = ontoAddKey;
|
|
66
|
+
exports.ontoCreateType = ontoCreateType;
|
|
66
67
|
exports.ontoRemoveKey = ontoRemoveKey;
|
|
67
68
|
exports.ontoPullToLocal = ontoPullToLocal;
|
|
68
69
|
const fs = __importStar(require("fs"));
|
|
@@ -1057,6 +1058,36 @@ async function ontoAddKey(pool, opts) {
|
|
|
1057
1058
|
client.release();
|
|
1058
1059
|
}
|
|
1059
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Create a new ontology type
|
|
1063
|
+
*/
|
|
1064
|
+
async function ontoCreateType(pool, opts) {
|
|
1065
|
+
// kebab-case 검증
|
|
1066
|
+
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(opts.type_key)) {
|
|
1067
|
+
return { success: false, error: `타입 키 '${opts.type_key}'이(가) 유효하지 않습니다. kebab-case 소문자만 사용 가능 (예: my-type)` };
|
|
1068
|
+
}
|
|
1069
|
+
const client = await pool.connect();
|
|
1070
|
+
try {
|
|
1071
|
+
// 중복 확인
|
|
1072
|
+
const dupCheck = await client.query("SELECT type_key FROM semo.ontology_types WHERE type_key = $1", [opts.type_key]);
|
|
1073
|
+
if (dupCheck.rows.length > 0) {
|
|
1074
|
+
return { success: false, error: `타입 '${opts.type_key}'은(는) 이미 존재합니다.` };
|
|
1075
|
+
}
|
|
1076
|
+
await client.query(`INSERT INTO semo.ontology_types (type_key, schema, description)
|
|
1077
|
+
VALUES ($1, $2, $3)`, [
|
|
1078
|
+
opts.type_key,
|
|
1079
|
+
JSON.stringify(opts.schema || {}),
|
|
1080
|
+
opts.description || opts.type_key,
|
|
1081
|
+
]);
|
|
1082
|
+
return { success: true };
|
|
1083
|
+
}
|
|
1084
|
+
catch (err) {
|
|
1085
|
+
return { success: false, error: String(err) };
|
|
1086
|
+
}
|
|
1087
|
+
finally {
|
|
1088
|
+
client.release();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1060
1091
|
/**
|
|
1061
1092
|
* Remove a key from a type schema
|
|
1062
1093
|
*/
|
package/dist/slack-notify.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Slack 알림 유틸리티
|
|
3
3
|
*
|
|
4
4
|
* SLACK_WEBHOOK 환경변수에서 URL을 읽어 알림 전송.
|
|
5
|
-
* ~/.semo
|
|
5
|
+
* ~/.claude/semo/.env에서 자동 로드됨 (database.ts loadSemoEnv).
|
|
6
6
|
*/
|
|
7
7
|
export declare function sendSlackNotification(message: string, webhookUrl?: string): Promise<boolean>;
|
|
8
8
|
export declare function formatTestFailureMessage(suiteId: string, runId: string, pass: number, fail: number, warn: number, failedLabels: string[]): string;
|
package/dist/slack-notify.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Slack 알림 유틸리티
|
|
4
4
|
*
|
|
5
5
|
* SLACK_WEBHOOK 환경변수에서 URL을 읽어 알림 전송.
|
|
6
|
-
* ~/.semo
|
|
6
|
+
* ~/.claude/semo/.env에서 자동 로드됨 (database.ts loadSemoEnv).
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.sendSlackNotification = sendSlackNotification;
|