@team-semicolon/semo-cli 4.1.5 → 4.3.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/memory.d.ts +8 -0
- package/dist/commands/memory.js +297 -0
- 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 +42 -3
- package/dist/database.js +129 -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 +197 -0
- package/dist/index.js +515 -821
- package/dist/kb.d.ts +40 -39
- package/dist/kb.js +185 -176
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,11 +56,15 @@ const child_process_1 = require("child_process");
|
|
|
56
56
|
const fs = __importStar(require("fs"));
|
|
57
57
|
const path = __importStar(require("path"));
|
|
58
58
|
const os = __importStar(require("os"));
|
|
59
|
+
const env_parser_1 = require("./env-parser");
|
|
59
60
|
const database_1 = require("./database");
|
|
60
61
|
const context_1 = require("./commands/context");
|
|
61
62
|
const bots_1 = require("./commands/bots");
|
|
62
63
|
const get_1 = require("./commands/get");
|
|
63
64
|
const sessions_1 = require("./commands/sessions");
|
|
65
|
+
const db_1 = require("./commands/db");
|
|
66
|
+
const memory_1 = require("./commands/memory");
|
|
67
|
+
const global_cache_1 = require("./global-cache");
|
|
64
68
|
const PACKAGE_NAME = "@team-semicolon/semo-cli";
|
|
65
69
|
// package.json에서 버전 동적 로드
|
|
66
70
|
function getCliVersion() {
|
|
@@ -133,7 +137,7 @@ async function getRemotePackageVersion(packagePath) {
|
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
/**
|
|
136
|
-
* semo-core
|
|
140
|
+
* semo-core 등 원격 버전 가져오기 (semo-system/ 하위 경로)
|
|
137
141
|
*/
|
|
138
142
|
async function getRemoteCoreVersion(type) {
|
|
139
143
|
try {
|
|
@@ -159,7 +163,7 @@ async function showVersionComparison(cwd) {
|
|
|
159
163
|
// 1. CLI 버전 비교
|
|
160
164
|
const currentCliVersion = VERSION;
|
|
161
165
|
const latestCliVersion = await getLatestVersion();
|
|
162
|
-
// 2. semo-core
|
|
166
|
+
// 2. semo-core 버전 비교
|
|
163
167
|
const semoSystemDir = path.join(cwd, "semo-system");
|
|
164
168
|
const hasSemoSystem = fs.existsSync(semoSystemDir);
|
|
165
169
|
const versionInfos = [];
|
|
@@ -171,15 +175,14 @@ async function showVersionComparison(cwd) {
|
|
|
171
175
|
needsUpdate: latestCliVersion ? isVersionLower(currentCliVersion, latestCliVersion) : false,
|
|
172
176
|
level: 0,
|
|
173
177
|
});
|
|
174
|
-
// 레거시 환경 경고 (루트에 semo-core
|
|
178
|
+
// 레거시 환경 경고 (루트에 semo-core가 있는 경우)
|
|
175
179
|
const hasLegacyCore = fs.existsSync(path.join(cwd, "semo-core"));
|
|
176
|
-
|
|
177
|
-
if (hasLegacyCore || hasLegacySkills) {
|
|
180
|
+
if (hasLegacyCore) {
|
|
178
181
|
spinner.warn("레거시 환경 감지됨");
|
|
179
182
|
console.log(chalk_1.default.yellow("\n ⚠️ 구버전 SEMO 구조가 감지되었습니다."));
|
|
180
|
-
console.log(chalk_1.default.gray(" 루트에 semo-core
|
|
183
|
+
console.log(chalk_1.default.gray(" 루트에 semo-core/가 있습니다."));
|
|
181
184
|
console.log(chalk_1.default.cyan("\n 💡 마이그레이션 방법:"));
|
|
182
|
-
console.log(chalk_1.default.gray(" 1. 기존 semo-core
|
|
185
|
+
console.log(chalk_1.default.gray(" 1. 기존 semo-core/ 폴더 삭제"));
|
|
183
186
|
console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
|
|
184
187
|
console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
|
|
185
188
|
console.log(chalk_1.default.gray(" 또는: semo migrate --force\n"));
|
|
@@ -198,19 +201,7 @@ async function showVersionComparison(cwd) {
|
|
|
198
201
|
level: 0,
|
|
199
202
|
});
|
|
200
203
|
}
|
|
201
|
-
// semo-skills
|
|
202
|
-
const skillsPathSystem = path.join(semoSystemDir, "semo-skills", "VERSION");
|
|
203
|
-
if (fs.existsSync(skillsPathSystem)) {
|
|
204
|
-
const localSkills = fs.readFileSync(skillsPathSystem, "utf-8").trim();
|
|
205
|
-
const remoteSkills = await getRemoteCoreVersion("semo-skills");
|
|
206
|
-
versionInfos.push({
|
|
207
|
-
name: "semo-skills",
|
|
208
|
-
local: localSkills,
|
|
209
|
-
remote: remoteSkills,
|
|
210
|
-
needsUpdate: remoteSkills ? isVersionLower(localSkills, remoteSkills) : false,
|
|
211
|
-
level: 0,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
204
|
+
// semo-skills 제거됨 — 중앙 DB 단일 SoT
|
|
214
205
|
// semo-agents (semo-system/ 내부)
|
|
215
206
|
const agentsPathSystem = path.join(semoSystemDir, "semo-agents", "VERSION");
|
|
216
207
|
if (fs.existsSync(agentsPathSystem)) {
|
|
@@ -384,13 +375,13 @@ function isSymlinkValid(linkPath) {
|
|
|
384
375
|
}
|
|
385
376
|
/**
|
|
386
377
|
* 레거시 SEMO 환경을 감지합니다.
|
|
387
|
-
* 레거시: 프로젝트 루트에 semo-core
|
|
378
|
+
* 레거시: 프로젝트 루트에 semo-core/ 가 직접 있는 경우
|
|
388
379
|
* 신규: semo-system/ 하위에 있는 경우
|
|
389
380
|
*/
|
|
390
381
|
function detectLegacyEnvironment(cwd) {
|
|
391
382
|
const legacyPaths = [];
|
|
392
383
|
// 루트에 직접 있는 레거시 디렉토리 확인
|
|
393
|
-
const legacyDirs = ["semo-core", "
|
|
384
|
+
const legacyDirs = ["semo-core", "sax-core", "sax-skills"];
|
|
394
385
|
for (const dir of legacyDirs) {
|
|
395
386
|
const dirPath = path.join(cwd, dir);
|
|
396
387
|
if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
|
|
@@ -452,7 +443,7 @@ async function migrateLegacyEnvironment(cwd) {
|
|
|
452
443
|
if (!shouldMigrate) {
|
|
453
444
|
console.log(chalk_1.default.yellow("\n마이그레이션이 취소되었습니다."));
|
|
454
445
|
console.log(chalk_1.default.gray("💡 수동 마이그레이션 방법:"));
|
|
455
|
-
console.log(chalk_1.default.gray(" 1. 기존
|
|
446
|
+
console.log(chalk_1.default.gray(" 1. 기존 레거시 폴더 삭제"));
|
|
456
447
|
console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
|
|
457
448
|
console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
|
|
458
449
|
return false;
|
|
@@ -460,7 +451,7 @@ async function migrateLegacyEnvironment(cwd) {
|
|
|
460
451
|
const spinner = (0, ora_1.default)("레거시 환경 마이그레이션 중...").start();
|
|
461
452
|
try {
|
|
462
453
|
// 1. 루트의 레거시 디렉토리 삭제
|
|
463
|
-
const legacyDirs = ["semo-core", "
|
|
454
|
+
const legacyDirs = ["semo-core", "sax-core", "sax-skills"];
|
|
464
455
|
for (const dir of legacyDirs) {
|
|
465
456
|
const dirPath = path.join(cwd, dir);
|
|
466
457
|
if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
|
|
@@ -579,21 +570,6 @@ async function showVersionInfo() {
|
|
|
579
570
|
level: 0,
|
|
580
571
|
});
|
|
581
572
|
}
|
|
582
|
-
// 3. semo-skills 버전 (루트 또는 semo-system 내부)
|
|
583
|
-
const skillsPathRoot = path.join(cwd, "semo-skills", "VERSION");
|
|
584
|
-
const skillsPathSystem = path.join(cwd, "semo-system", "semo-skills", "VERSION");
|
|
585
|
-
const skillsPath = fs.existsSync(skillsPathRoot) ? skillsPathRoot : skillsPathSystem;
|
|
586
|
-
if (fs.existsSync(skillsPath)) {
|
|
587
|
-
const localSkills = fs.readFileSync(skillsPath, "utf-8").trim();
|
|
588
|
-
const remoteSkills = await getRemoteCoreVersion("semo-skills");
|
|
589
|
-
versionInfos.push({
|
|
590
|
-
name: "semo-skills",
|
|
591
|
-
local: localSkills,
|
|
592
|
-
remote: remoteSkills,
|
|
593
|
-
needsUpdate: remoteSkills ? isVersionLower(localSkills, remoteSkills) : false,
|
|
594
|
-
level: 0,
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
573
|
// 결과 출력
|
|
598
574
|
const needsUpdateCount = versionInfos.filter(v => v.needsUpdate).length;
|
|
599
575
|
if (versionInfos.length === 1) {
|
|
@@ -676,11 +652,6 @@ async function confirmOverwrite(itemName, itemPath) {
|
|
|
676
652
|
if (!fs.existsSync(itemPath)) {
|
|
677
653
|
return true;
|
|
678
654
|
}
|
|
679
|
-
// 비인터랙티브 환경(CI, 파이프) — 덮어쓰지 않고 기존 파일 유지
|
|
680
|
-
if (!process.stdin.isTTY) {
|
|
681
|
-
console.log(chalk_1.default.gray(` → ${itemName} 이미 존재 (비인터랙티브 모드: 건너뜀)`));
|
|
682
|
-
return false;
|
|
683
|
-
}
|
|
684
655
|
const { shouldOverwrite } = await inquirer_1.default.prompt([
|
|
685
656
|
{
|
|
686
657
|
type: "confirm",
|
|
@@ -769,36 +740,82 @@ async function showToolsStatus() {
|
|
|
769
740
|
}
|
|
770
741
|
return true;
|
|
771
742
|
}
|
|
772
|
-
// ===
|
|
743
|
+
// === 글로벌 설정 체크 ===
|
|
744
|
+
function isGlobalSetupDone() {
|
|
745
|
+
const home = os.homedir();
|
|
746
|
+
return (fs.existsSync(path.join(home, ".semo.env")) &&
|
|
747
|
+
fs.existsSync(path.join(home, ".claude", "skills")));
|
|
748
|
+
}
|
|
749
|
+
// === onboarding 명령어 (글로벌 1회 설정) ===
|
|
773
750
|
program
|
|
774
|
-
.command("
|
|
775
|
-
.description("
|
|
751
|
+
.command("onboarding")
|
|
752
|
+
.description("글로벌 SEMO 설정 (머신당 1회) — ~/.claude/, ~/.semo.env")
|
|
753
|
+
.option("--credentials-gist <gistId>", "Private GitHub Gist에서 DB 접속정보 가져오기")
|
|
776
754
|
.option("-f, --force", "기존 설정 덮어쓰기")
|
|
777
755
|
.option("--skip-mcp", "MCP 설정 생략")
|
|
756
|
+
.action(async (options) => {
|
|
757
|
+
console.log(chalk_1.default.cyan.bold("\n🏠 SEMO 글로벌 온보딩\n"));
|
|
758
|
+
console.log(chalk_1.default.gray(" 대상: ~/.claude/, ~/.semo.env (머신당 1회)\n"));
|
|
759
|
+
// 1. ~/.semo.env DB 접속 설정
|
|
760
|
+
await setupSemoEnv(options.credentialsGist, options.force);
|
|
761
|
+
// 2. DB health check
|
|
762
|
+
const spinner = (0, ora_1.default)("DB 연결 확인 중...").start();
|
|
763
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
764
|
+
if (connected) {
|
|
765
|
+
spinner.succeed("DB 연결 확인됨");
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
spinner.warn("DB 연결 실패 — 스킬/커맨드/에이전트 설치를 건너뜁니다");
|
|
769
|
+
console.log(chalk_1.default.gray(" ~/.semo.env를 확인하고 다시 시도하세요: semo onboarding\n"));
|
|
770
|
+
await (0, database_1.closeConnection)();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
// 3. Standard 설치 (DB → ~/.claude/skills, commands, agents)
|
|
774
|
+
await setupStandardGlobal();
|
|
775
|
+
// 4. Hooks 설치 (프로젝트 무관)
|
|
776
|
+
await setupHooks(false);
|
|
777
|
+
// 5. MCP 설정 (글로벌 공통 서버)
|
|
778
|
+
if (!options.skipMcp) {
|
|
779
|
+
await setupMCP(os.homedir(), [], options.force || false);
|
|
780
|
+
}
|
|
781
|
+
// 6. semo-kb MCP 유저레벨 등록
|
|
782
|
+
if (!options.skipMcp) {
|
|
783
|
+
await setupSemoKbMcp();
|
|
784
|
+
}
|
|
785
|
+
// 7. 글로벌 CLAUDE.md에 KB-First 규칙 주입
|
|
786
|
+
await injectKbFirstToGlobalClaudeMd();
|
|
787
|
+
await (0, database_1.closeConnection)();
|
|
788
|
+
// 결과 요약
|
|
789
|
+
console.log(chalk_1.default.green.bold("\n✅ SEMO 글로벌 온보딩 완료!\n"));
|
|
790
|
+
console.log(chalk_1.default.cyan("설치된 구성:"));
|
|
791
|
+
console.log(chalk_1.default.gray(" ~/.semo.env DB 접속정보 (권한 600)"));
|
|
792
|
+
console.log(chalk_1.default.gray(" ~/.claude/skills/ 팀 스킬 (DB 기반)"));
|
|
793
|
+
console.log(chalk_1.default.gray(" ~/.claude/commands/ 팀 커맨드 (DB 기반)"));
|
|
794
|
+
console.log(chalk_1.default.gray(" ~/.claude/agents/ 팀 에이전트 (DB 기반, dedup)"));
|
|
795
|
+
console.log(chalk_1.default.gray(" ~/.claude/settings.local.json SessionStart/Stop 훅"));
|
|
796
|
+
console.log(chalk_1.default.gray(" ~/.claude/settings.json semo-kb MCP (유저레벨, KB-First SoT)"));
|
|
797
|
+
console.log(chalk_1.default.cyan("\n다음 단계:"));
|
|
798
|
+
console.log(chalk_1.default.gray(" 프로젝트 디렉토리에서 'semo init'을 실행하세요."));
|
|
799
|
+
console.log();
|
|
800
|
+
});
|
|
801
|
+
// === init 명령어 (프로젝트별 설정) ===
|
|
802
|
+
program
|
|
803
|
+
.command("init")
|
|
804
|
+
.description("현재 프로젝트에 SEMO 프로젝트 설정을 합니다 (글로벌: semo onboarding)")
|
|
805
|
+
.option("-f, --force", "기존 설정 덮어쓰기")
|
|
778
806
|
.option("--no-gitignore", ".gitignore 수정 생략")
|
|
779
|
-
.option("--migrate", "레거시 환경 강제 마이그레이션")
|
|
780
|
-
.option("--seed-skills", "semo-system/semo-skills/ → semo.skills DB 초기 시딩")
|
|
781
|
-
.option("--credentials-gist <gistId>", "Private GitHub Gist에서 팀 DB 접속정보 자동 가져오기")
|
|
782
807
|
.action(async (options) => {
|
|
783
|
-
console.log(chalk_1.default.cyan.bold("\n
|
|
808
|
+
console.log(chalk_1.default.cyan.bold("\n📁 SEMO 프로젝트 설정\n"));
|
|
784
809
|
const cwd = process.cwd();
|
|
785
|
-
// 0.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
process.exit(0);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
// 1. 필수 도구 확인
|
|
796
|
-
const shouldContinue = await showToolsStatus();
|
|
797
|
-
if (!shouldContinue) {
|
|
798
|
-
console.log(chalk_1.default.yellow("\n설치가 취소되었습니다. 필수 도구 설치 후 다시 시도하세요.\n"));
|
|
799
|
-
process.exit(0);
|
|
810
|
+
// 0. 글로벌 설정 확인
|
|
811
|
+
if (!isGlobalSetupDone()) {
|
|
812
|
+
console.log(chalk_1.default.yellow("⚠ 글로벌 설정이 완료되지 않았습니다."));
|
|
813
|
+
console.log(chalk_1.default.gray(" 먼저 'semo onboarding'을 실행하세요.\n"));
|
|
814
|
+
console.log(chalk_1.default.gray(" 이 머신에서 처음 SEMO를 사용하시나요?"));
|
|
815
|
+
console.log(chalk_1.default.cyan(" → semo onboarding --credentials-gist <GIST_ID>\n"));
|
|
816
|
+
process.exit(1);
|
|
800
817
|
}
|
|
801
|
-
// 1.
|
|
818
|
+
// 1. Git 레포지토리 확인
|
|
802
819
|
const spinner = (0, ora_1.default)("Git 레포지토리 확인 중...").start();
|
|
803
820
|
try {
|
|
804
821
|
(0, child_process_1.execSync)("git rev-parse --git-dir", { cwd, stdio: "pipe" });
|
|
@@ -814,64 +831,48 @@ program
|
|
|
814
831
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
815
832
|
console.log(chalk_1.default.green("\n✓ .claude/ 디렉토리 생성됨"));
|
|
816
833
|
}
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
834
|
+
// 3. 로컬 스킬 경고 (기존 프로젝트 호환)
|
|
835
|
+
const localSkillsDir = path.join(claudeDir, "skills");
|
|
836
|
+
if (fs.existsSync(localSkillsDir)) {
|
|
837
|
+
try {
|
|
838
|
+
const localSkills = fs.readdirSync(localSkillsDir).filter(f => fs.statSync(path.join(localSkillsDir, f)).isDirectory());
|
|
839
|
+
if (localSkills.length > 0) {
|
|
840
|
+
console.log(chalk_1.default.yellow(`\nℹ 프로젝트 로컬 스킬이 감지되었습니다 (${localSkills.length}개).`));
|
|
841
|
+
console.log(chalk_1.default.gray(" 글로벌 스킬(~/.claude/skills/)이 우선 적용됩니다."));
|
|
842
|
+
console.log(chalk_1.default.gray(" 로컬 스킬을 제거하려면: rm -rf .claude/skills/ .claude/commands/ .claude/agents/\n"));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
// ignore
|
|
847
|
+
}
|
|
824
848
|
}
|
|
825
|
-
//
|
|
849
|
+
// 4. Context Mesh 초기화
|
|
826
850
|
await setupContextMesh(cwd);
|
|
851
|
+
// 5. CLAUDE.md 생성 (프로젝트 규칙만, 스킬 목록 없음)
|
|
852
|
+
await setupClaudeMd(cwd, [], options.force || false);
|
|
827
853
|
// 6. .gitignore 업데이트
|
|
828
854
|
if (options.gitignore !== false) {
|
|
829
855
|
updateGitignore(cwd);
|
|
830
856
|
}
|
|
831
|
-
// 7. Hooks 설치
|
|
832
|
-
await setupHooks(cwd, false);
|
|
833
|
-
// 8. CLAUDE.md 생성
|
|
834
|
-
await setupClaudeMd(cwd, [], options.force);
|
|
835
|
-
// 9. 설치 검증
|
|
836
|
-
const verificationResult = verifyInstallation(cwd, []);
|
|
837
|
-
printVerificationResult(verificationResult);
|
|
838
|
-
// 10. Skills DB 시딩 (--seed-skills 옵션)
|
|
839
|
-
if (options.seedSkills) {
|
|
840
|
-
console.log(chalk_1.default.cyan("\n🌱 스킬 DB 시딩 (--seed-skills)"));
|
|
841
|
-
const semoSystemDir = path.join(cwd, "semo-system");
|
|
842
|
-
await seedSkillsToDb(semoSystemDir);
|
|
843
|
-
}
|
|
844
857
|
// 완료 메시지
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
}
|
|
851
|
-
console.log(chalk_1.default.cyan("설치된 구성:"));
|
|
852
|
-
console.log(chalk_1.default.gray(" ✓ .claude/skills/ (DB 기반 스킬)"));
|
|
853
|
-
console.log(chalk_1.default.gray(" ✓ .claude/agents/ (DB 기반 에이전트)"));
|
|
854
|
-
console.log(chalk_1.default.gray(" ✓ .claude/commands/ (슬래시 커맨드)"));
|
|
855
|
-
console.log(chalk_1.default.gray(" ✓ .claude/memory/ (컨텍스트 동기화)"));
|
|
856
|
-
console.log(chalk_1.default.gray(" ✓ ~/.claude/settings.local.json (훅 등록)"));
|
|
858
|
+
console.log(chalk_1.default.green.bold("\n✅ SEMO 프로젝트 설정 완료!\n"));
|
|
859
|
+
console.log(chalk_1.default.cyan("생성된 파일:"));
|
|
860
|
+
console.log(chalk_1.default.gray(" {cwd}/.claude/CLAUDE.md 프로젝트 규칙"));
|
|
861
|
+
console.log(chalk_1.default.gray(" {cwd}/.claude/memory/context.md 프로젝트 상태"));
|
|
862
|
+
console.log(chalk_1.default.gray(" {cwd}/.claude/memory/decisions.md ADR"));
|
|
863
|
+
console.log(chalk_1.default.gray(" {cwd}/.claude/memory/projects.md 프로젝트 맵"));
|
|
857
864
|
console.log(chalk_1.default.cyan("\n다음 단계:"));
|
|
858
865
|
console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기 (SessionStart 훅이 자동 sync)"));
|
|
859
866
|
console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
|
|
860
867
|
console.log(chalk_1.default.gray(" 3. /SEMO:help로 도움말 확인"));
|
|
861
868
|
console.log();
|
|
862
|
-
console.log(chalk_1.default.gray(" DB 접속정보 변경: nano ~/.semo.env"));
|
|
863
|
-
console.log();
|
|
864
869
|
});
|
|
865
|
-
// === Standard 설치 (DB
|
|
866
|
-
async function
|
|
867
|
-
|
|
868
|
-
console.log(chalk_1.default.
|
|
869
|
-
console.log(chalk_1.default.gray(" 스킬: DB에서 조회하여 파일 생성"));
|
|
870
|
-
console.log(chalk_1.default.gray(" 커맨드: DB에서 조회하여 파일 생성"));
|
|
871
|
-
console.log(chalk_1.default.gray(" 에이전트: DB에서 조회하여 파일 생성\n"));
|
|
870
|
+
// === Standard 설치 (DB 기반, 글로벌 ~/.claude/) ===
|
|
871
|
+
async function setupStandardGlobal() {
|
|
872
|
+
console.log(chalk_1.default.cyan("\n📚 Standard 설치 (DB → ~/.claude/)"));
|
|
873
|
+
console.log(chalk_1.default.gray(" 스킬/커맨드/에이전트를 글로벌에 설치\n"));
|
|
872
874
|
const spinner = (0, ora_1.default)("DB에서 스킬/커맨드/에이전트 조회 중...").start();
|
|
873
875
|
try {
|
|
874
|
-
// DB 연결 확인
|
|
875
876
|
const connected = await (0, database_1.isDbConnected)();
|
|
876
877
|
if (connected) {
|
|
877
878
|
spinner.text = "DB 연결 성공, 데이터 조회 중...";
|
|
@@ -879,174 +880,18 @@ async function setupStandard(cwd, force) {
|
|
|
879
880
|
else {
|
|
880
881
|
spinner.text = "DB 연결 실패, 폴백 데이터 사용 중...";
|
|
881
882
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
removeRecursive(skillsDir);
|
|
888
|
-
}
|
|
889
|
-
fs.mkdirSync(skillsDir, { recursive: true });
|
|
890
|
-
const skills = await (0, database_1.getActiveSkills)();
|
|
891
|
-
for (const skill of skills) {
|
|
892
|
-
const skillFolder = path.join(skillsDir, skill.name);
|
|
893
|
-
fs.mkdirSync(skillFolder, { recursive: true });
|
|
894
|
-
fs.writeFileSync(path.join(skillFolder, "SKILL.md"), skill.content);
|
|
895
|
-
}
|
|
896
|
-
console.log(chalk_1.default.green(` ✓ skills 설치 완료 (${skills.length}개)`));
|
|
897
|
-
// 2. 커맨드 설치
|
|
898
|
-
const commandsDir = path.join(claudeDir, "commands");
|
|
899
|
-
if (force && fs.existsSync(commandsDir)) {
|
|
900
|
-
removeRecursive(commandsDir);
|
|
901
|
-
}
|
|
902
|
-
fs.mkdirSync(commandsDir, { recursive: true });
|
|
903
|
-
const commands = await (0, database_1.getCommands)();
|
|
904
|
-
// 폴더별로 그룹핑
|
|
905
|
-
const commandsByFolder = {};
|
|
906
|
-
for (const cmd of commands) {
|
|
907
|
-
if (!commandsByFolder[cmd.folder]) {
|
|
908
|
-
commandsByFolder[cmd.folder] = [];
|
|
909
|
-
}
|
|
910
|
-
commandsByFolder[cmd.folder].push(cmd);
|
|
911
|
-
}
|
|
912
|
-
let cmdCount = 0;
|
|
913
|
-
for (const [folder, cmds] of Object.entries(commandsByFolder)) {
|
|
914
|
-
const folderPath = path.join(commandsDir, folder);
|
|
915
|
-
fs.mkdirSync(folderPath, { recursive: true });
|
|
916
|
-
for (const cmd of cmds) {
|
|
917
|
-
fs.writeFileSync(path.join(folderPath, `${cmd.name}.md`), cmd.content);
|
|
918
|
-
cmdCount++;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
console.log(chalk_1.default.green(` ✓ commands 설치 완료 (${cmdCount}개)`));
|
|
922
|
-
// 3. 에이전트 설치
|
|
923
|
-
const agentsDir = path.join(claudeDir, "agents");
|
|
924
|
-
if (force && fs.existsSync(agentsDir)) {
|
|
925
|
-
removeRecursive(agentsDir);
|
|
926
|
-
}
|
|
927
|
-
fs.mkdirSync(agentsDir, { recursive: true });
|
|
928
|
-
const agents = await (0, database_1.getAgents)();
|
|
929
|
-
for (const agent of agents) {
|
|
930
|
-
const agentFolder = path.join(agentsDir, agent.name);
|
|
931
|
-
fs.mkdirSync(agentFolder, { recursive: true });
|
|
932
|
-
fs.writeFileSync(path.join(agentFolder, `${agent.name}.md`), agent.content);
|
|
933
|
-
}
|
|
934
|
-
console.log(chalk_1.default.green(` ✓ agents 설치 완료 (${agents.length}개)`));
|
|
935
|
-
spinner.succeed("Standard 설치 완료 (DB 기반)");
|
|
883
|
+
const result = await (0, global_cache_1.syncGlobalCache)();
|
|
884
|
+
console.log(chalk_1.default.green(` ✓ skills 설치 완료 (${result.skills}개)`));
|
|
885
|
+
console.log(chalk_1.default.green(` ✓ commands 설치 완료 (${result.commands}개)`));
|
|
886
|
+
console.log(chalk_1.default.green(` ✓ agents 설치 완료 (${result.agents}개)`));
|
|
887
|
+
spinner.succeed("Standard 설치 완료 (DB → ~/.claude/)");
|
|
936
888
|
}
|
|
937
889
|
catch (error) {
|
|
938
890
|
spinner.fail("Standard 설치 실패");
|
|
939
891
|
console.error(chalk_1.default.red(` ${error}`));
|
|
940
892
|
}
|
|
941
893
|
}
|
|
942
|
-
//
|
|
943
|
-
async function generateClaudeMd(cwd) {
|
|
944
|
-
console.log(chalk_1.default.cyan("\n📄 CLAUDE.md 생성"));
|
|
945
|
-
const claudeMdPath = path.join(cwd, ".claude", "CLAUDE.md");
|
|
946
|
-
const skills = await (0, database_1.getActiveSkills)();
|
|
947
|
-
const skillCategories = await (0, database_1.getSkillCountByCategory)();
|
|
948
|
-
const skillList = Object.entries(skillCategories)
|
|
949
|
-
.map(([cat, count]) => ` - ${cat}: ${count}개`)
|
|
950
|
-
.join("\n");
|
|
951
|
-
const claudeMdContent = `# SEMO Project Configuration
|
|
952
|
-
|
|
953
|
-
> SEMO (Semicolon Orchestrate) - AI Agent Orchestration Framework v3.14.0
|
|
954
|
-
|
|
955
|
-
---
|
|
956
|
-
|
|
957
|
-
## 🔴 MANDATORY: Memory Context (항시 참조)
|
|
958
|
-
|
|
959
|
-
> **⚠️ 세션 시작 시 반드시 \`.claude/memory/\` 폴더의 파일들을 먼저 읽으세요. 예외 없음.**
|
|
960
|
-
|
|
961
|
-
### 필수 참조 파일
|
|
962
|
-
|
|
963
|
-
\`\`\`
|
|
964
|
-
.claude/memory/
|
|
965
|
-
├── context.md # 프로젝트 상태, 기술 스택, 진행 중 작업
|
|
966
|
-
├── decisions.md # 아키텍처 결정 기록 (ADR)
|
|
967
|
-
├── projects.md # GitHub Projects 설정
|
|
968
|
-
└── rules/ # 프로젝트별 커스텀 규칙
|
|
969
|
-
\`\`\`
|
|
970
|
-
|
|
971
|
-
**이 파일들은 세션의 컨텍스트를 유지하는 장기 기억입니다. 매 세션마다 반드시 읽고 시작하세요.**
|
|
972
|
-
|
|
973
|
-
---
|
|
974
|
-
|
|
975
|
-
## 🔴 MANDATORY: Orchestrator-First Execution
|
|
976
|
-
|
|
977
|
-
> **⚠️ 이 규칙은 모든 사용자 요청에 적용됩니다. 예외 없음.**
|
|
978
|
-
|
|
979
|
-
### 실행 흐름 (필수)
|
|
980
|
-
|
|
981
|
-
\`\`\`
|
|
982
|
-
1. 사용자 요청 수신
|
|
983
|
-
2. Orchestrator가 의도 분석 후 적절한 Agent/Skill 라우팅
|
|
984
|
-
3. Agent/Skill이 작업 수행
|
|
985
|
-
4. 실행 결과 반환
|
|
986
|
-
\`\`\`
|
|
987
|
-
|
|
988
|
-
### Orchestrator 참조
|
|
989
|
-
|
|
990
|
-
**Primary Orchestrator**: \`.claude/agents/orchestrator/orchestrator.md\`
|
|
991
|
-
|
|
992
|
-
이 파일에서 라우팅 테이블, 의도 분류, 메시지 포맷을 확인하세요.
|
|
993
|
-
|
|
994
|
-
---
|
|
995
|
-
|
|
996
|
-
## 🔴 NON-NEGOTIABLE RULES
|
|
997
|
-
|
|
998
|
-
### 1. Orchestrator-First Policy
|
|
999
|
-
|
|
1000
|
-
> **모든 요청은 반드시 Orchestrator를 통해 라우팅됩니다. 직접 처리 금지.**
|
|
1001
|
-
|
|
1002
|
-
**직접 처리 금지 항목**:
|
|
1003
|
-
- 코드 작성/수정 → \`write-code\` 스킬
|
|
1004
|
-
- Git 커밋/푸시 → \`git-workflow\` 스킬
|
|
1005
|
-
- 품질 검증 → \`quality-gate\` 스킬
|
|
1006
|
-
|
|
1007
|
-
### 2. Pre-Commit Quality Gate
|
|
1008
|
-
|
|
1009
|
-
> **코드 변경이 포함된 커밋 전 반드시 Quality Gate를 통과해야 합니다.**
|
|
1010
|
-
|
|
1011
|
-
\`\`\`bash
|
|
1012
|
-
# 필수 검증 순서
|
|
1013
|
-
npm run lint # 1. ESLint 검사
|
|
1014
|
-
npx tsc --noEmit # 2. TypeScript 타입 체크
|
|
1015
|
-
npm run build # 3. 빌드 검증
|
|
1016
|
-
\`\`\`
|
|
1017
|
-
|
|
1018
|
-
---
|
|
1019
|
-
|
|
1020
|
-
## 설치된 구성
|
|
1021
|
-
|
|
1022
|
-
### 스킬 (${skills.length}개)
|
|
1023
|
-
${skillList}
|
|
1024
|
-
|
|
1025
|
-
## 구조
|
|
1026
|
-
|
|
1027
|
-
\`\`\`
|
|
1028
|
-
.claude/
|
|
1029
|
-
├── settings.json # MCP 서버 설정
|
|
1030
|
-
├── agents/ # 에이전트 (DB 기반 설치)
|
|
1031
|
-
├── skills/ # 스킬 (DB 기반 설치)
|
|
1032
|
-
└── commands/ # 커맨드 (DB 기반 설치)
|
|
1033
|
-
\`\`\`
|
|
1034
|
-
|
|
1035
|
-
## 사용 가능한 커맨드
|
|
1036
|
-
|
|
1037
|
-
| 커맨드 | 설명 |
|
|
1038
|
-
|--------|------|
|
|
1039
|
-
| \`/SEMO:help\` | 도움말 |
|
|
1040
|
-
| \`/SEMO:dry-run {프롬프트}\` | 명령 검증 (라우팅 시뮬레이션) |
|
|
1041
|
-
| \`/SEMO-workflow:greenfield\` | Greenfield 워크플로우 시작 |
|
|
1042
|
-
|
|
1043
|
-
---
|
|
1044
|
-
|
|
1045
|
-
> Generated by SEMO CLI v3.14.0 (DB-based installation)
|
|
1046
|
-
`;
|
|
1047
|
-
fs.writeFileSync(claudeMdPath, claudeMdContent);
|
|
1048
|
-
console.log(chalk_1.default.green("✓ .claude/CLAUDE.md 생성됨"));
|
|
1049
|
-
}
|
|
894
|
+
// (generateClaudeMd removed — setupClaudeMd handles project CLAUDE.md generation)
|
|
1050
895
|
// === Standard 심볼릭 링크 (레거시 호환) ===
|
|
1051
896
|
async function createStandardSymlinks(cwd) {
|
|
1052
897
|
const claudeDir = path.join(cwd, ".claude");
|
|
@@ -1070,29 +915,9 @@ async function createStandardSymlinks(cwd) {
|
|
|
1070
915
|
}
|
|
1071
916
|
console.log(chalk_1.default.green(` ✓ .claude/agents/ (${agents.length}개 agent 링크됨)`));
|
|
1072
917
|
}
|
|
1073
|
-
// skills
|
|
918
|
+
// skills — 중앙 DB 단일 SoT (semo-skills 파일시스템 제거됨)
|
|
1074
919
|
const claudeSkillsDir = path.join(claudeDir, "skills");
|
|
1075
|
-
|
|
1076
|
-
if (fs.existsSync(coreSkillsDir)) {
|
|
1077
|
-
// 기존 심볼릭 링크면 삭제 (디렉토리로 변경)
|
|
1078
|
-
if (fs.existsSync(claudeSkillsDir) && fs.lstatSync(claudeSkillsDir).isSymbolicLink()) {
|
|
1079
|
-
removeRecursive(claudeSkillsDir);
|
|
1080
|
-
}
|
|
1081
|
-
fs.mkdirSync(claudeSkillsDir, { recursive: true });
|
|
1082
|
-
// DB에서 활성 스킬 목록 조회 (19개 핵심 스킬만)
|
|
1083
|
-
const activeSkillNames = await (0, database_1.getActiveSkillNames)();
|
|
1084
|
-
let linkedCount = 0;
|
|
1085
|
-
for (const skillName of activeSkillNames) {
|
|
1086
|
-
const skillLink = path.join(claudeSkillsDir, skillName);
|
|
1087
|
-
const skillTarget = path.join(coreSkillsDir, skillName);
|
|
1088
|
-
// 스킬 폴더가 존재하는 경우에만 링크
|
|
1089
|
-
if (fs.existsSync(skillTarget) && !fs.existsSync(skillLink)) {
|
|
1090
|
-
createSymlinkOrJunction(skillTarget, skillLink);
|
|
1091
|
-
linkedCount++;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
console.log(chalk_1.default.green(` ✓ .claude/skills/ (${linkedCount}개 skill 링크됨 - DB 기반)`));
|
|
1095
|
-
}
|
|
920
|
+
fs.mkdirSync(claudeSkillsDir, { recursive: true });
|
|
1096
921
|
// commands 링크
|
|
1097
922
|
const commandsDir = path.join(claudeDir, "commands");
|
|
1098
923
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
@@ -1183,15 +1008,10 @@ function verifyInstallation(cwd, installedExtensions = []) {
|
|
|
1183
1008
|
}
|
|
1184
1009
|
// === 레거시: semo-system 기반 설치 검증 ===
|
|
1185
1010
|
const coreDir = path.join(semoSystemDir, "semo-core");
|
|
1186
|
-
const skillsDir = path.join(semoSystemDir, "semo-skills");
|
|
1187
1011
|
if (!fs.existsSync(coreDir)) {
|
|
1188
1012
|
result.errors.push("semo-core가 설치되지 않았습니다");
|
|
1189
1013
|
result.success = false;
|
|
1190
1014
|
}
|
|
1191
|
-
if (!fs.existsSync(skillsDir)) {
|
|
1192
|
-
result.errors.push("semo-skills가 설치되지 않았습니다");
|
|
1193
|
-
result.success = false;
|
|
1194
|
-
}
|
|
1195
1015
|
// 2. agents 링크 검증 (isSymlinkValid 사용)
|
|
1196
1016
|
const claudeAgentsDir = path.join(claudeDir, "agents");
|
|
1197
1017
|
const coreAgentsDir = path.join(coreDir, "agents");
|
|
@@ -1218,31 +1038,7 @@ function verifyInstallation(cwd, installedExtensions = []) {
|
|
|
1218
1038
|
}
|
|
1219
1039
|
}
|
|
1220
1040
|
}
|
|
1221
|
-
// 3. skills
|
|
1222
|
-
if (fs.existsSync(skillsDir)) {
|
|
1223
|
-
const expectedSkills = fs.readdirSync(skillsDir).filter(f => fs.statSync(path.join(skillsDir, f)).isDirectory());
|
|
1224
|
-
result.stats.skills.expected = expectedSkills.length;
|
|
1225
|
-
const claudeSkillsDir = path.join(claudeDir, "skills");
|
|
1226
|
-
if (fs.existsSync(claudeSkillsDir)) {
|
|
1227
|
-
for (const skill of expectedSkills) {
|
|
1228
|
-
const linkPath = path.join(claudeSkillsDir, skill);
|
|
1229
|
-
try {
|
|
1230
|
-
if (fs.existsSync(linkPath) || fs.lstatSync(linkPath).isSymbolicLink()) {
|
|
1231
|
-
if (isSymlinkValid(linkPath)) {
|
|
1232
|
-
result.stats.skills.linked++;
|
|
1233
|
-
}
|
|
1234
|
-
else {
|
|
1235
|
-
result.stats.skills.broken++;
|
|
1236
|
-
result.warnings.push(`깨진 링크: .claude/skills/${skill}`);
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
catch {
|
|
1241
|
-
// 링크가 존재하지 않음
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1041
|
+
// 3. skills — 중앙 DB 단일 SoT (파일시스템 검증 불필요)
|
|
1246
1042
|
// 4. commands 검증 (isSymlinkValid 사용)
|
|
1247
1043
|
const semoCommandsLink = path.join(claudeDir, "commands", "SEMO");
|
|
1248
1044
|
try {
|
|
@@ -1372,110 +1168,156 @@ const BASE_MCP_SERVERS = [
|
|
|
1372
1168
|
name: "context7",
|
|
1373
1169
|
command: "npx",
|
|
1374
1170
|
args: ["-y", "@upstash/context7-mcp"],
|
|
1171
|
+
scope: "user",
|
|
1375
1172
|
},
|
|
1376
1173
|
{
|
|
1377
1174
|
name: "sequential-thinking",
|
|
1378
1175
|
command: "npx",
|
|
1379
1176
|
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
|
1177
|
+
scope: "user",
|
|
1380
1178
|
},
|
|
1381
1179
|
{
|
|
1382
1180
|
name: "playwright",
|
|
1383
1181
|
command: "npx",
|
|
1384
1182
|
args: ["-y", "@anthropic-ai/mcp-server-playwright"],
|
|
1183
|
+
scope: "user",
|
|
1385
1184
|
},
|
|
1386
1185
|
{
|
|
1387
1186
|
name: "github",
|
|
1388
1187
|
command: "npx",
|
|
1389
1188
|
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
1189
|
+
scope: "user",
|
|
1190
|
+
},
|
|
1191
|
+
];
|
|
1192
|
+
const SEMO_CREDENTIALS = [
|
|
1193
|
+
{
|
|
1194
|
+
key: "DATABASE_URL",
|
|
1195
|
+
required: true,
|
|
1196
|
+
sensitive: true,
|
|
1197
|
+
description: "팀 코어 PostgreSQL 연결 URL",
|
|
1198
|
+
promptMessage: "DATABASE_URL:",
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
key: "OPENAI_API_KEY",
|
|
1202
|
+
required: false,
|
|
1203
|
+
sensitive: true,
|
|
1204
|
+
description: "OpenAI API 키 (KB 임베딩용)",
|
|
1205
|
+
promptMessage: "OPENAI_API_KEY (없으면 Enter):",
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
key: "SLACK_WEBHOOK",
|
|
1209
|
+
required: false,
|
|
1210
|
+
sensitive: false,
|
|
1211
|
+
description: "Slack 알림 Webhook (선택)",
|
|
1212
|
+
promptMessage: "SLACK_WEBHOOK (없으면 Enter):",
|
|
1390
1213
|
},
|
|
1391
1214
|
];
|
|
1392
|
-
|
|
1393
|
-
function writeSemoEnvFile(dbUrl, slackWebhook = "") {
|
|
1215
|
+
function writeSemoEnvFile(creds) {
|
|
1394
1216
|
const envFile = path.join(os.homedir(), ".semo.env");
|
|
1395
|
-
const
|
|
1396
|
-
#
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1217
|
+
const lines = [
|
|
1218
|
+
"# SEMO 환경변수 — 모든 컨텍스트에서 자동 로드됨",
|
|
1219
|
+
"# (Claude Code 앱, OpenClaw LaunchAgent, cron 등)",
|
|
1220
|
+
"",
|
|
1221
|
+
];
|
|
1222
|
+
// 레지스트리 키 먼저 (순서 보장)
|
|
1223
|
+
for (const def of SEMO_CREDENTIALS) {
|
|
1224
|
+
const val = creds[def.key] || "";
|
|
1225
|
+
lines.push(`# ${def.description}`);
|
|
1226
|
+
lines.push(`${def.key}='${val}'`);
|
|
1227
|
+
lines.push("");
|
|
1228
|
+
}
|
|
1229
|
+
// 레지스트리 외 추가 키
|
|
1230
|
+
for (const [k, v] of Object.entries(creds)) {
|
|
1231
|
+
if (!SEMO_CREDENTIALS.some((d) => d.key === k)) {
|
|
1232
|
+
lines.push(`${k}='${v}'`);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
lines.push("");
|
|
1236
|
+
fs.writeFileSync(envFile, lines.join("\n"), { mode: 0o600 });
|
|
1404
1237
|
}
|
|
1405
|
-
function
|
|
1238
|
+
function readSemoEnvCreds() {
|
|
1406
1239
|
const envFile = path.join(os.homedir(), ".semo.env");
|
|
1407
1240
|
if (!fs.existsSync(envFile))
|
|
1408
|
-
return
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1241
|
+
return {};
|
|
1242
|
+
try {
|
|
1243
|
+
return (0, env_parser_1.parseEnvContent)(fs.readFileSync(envFile, "utf-8"));
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1246
|
+
return {};
|
|
1247
|
+
}
|
|
1412
1248
|
}
|
|
1413
|
-
function
|
|
1249
|
+
function fetchCredsFromGist(gistId) {
|
|
1414
1250
|
try {
|
|
1415
1251
|
const raw = (0, child_process_1.execSync)(`gh gist view ${gistId} --raw`, {
|
|
1416
1252
|
encoding: "utf-8",
|
|
1417
1253
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1418
1254
|
timeout: 8000,
|
|
1419
1255
|
});
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
return match ? match[1].trim() : null;
|
|
1256
|
+
const creds = (0, env_parser_1.parseEnvContent)(raw);
|
|
1257
|
+
return Object.keys(creds).length > 0 ? creds : null;
|
|
1423
1258
|
}
|
|
1424
1259
|
catch {
|
|
1425
1260
|
return null;
|
|
1426
1261
|
}
|
|
1427
1262
|
}
|
|
1428
|
-
async function setupSemoEnv(credentialsGist) {
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
console.log(chalk_1.default.green(" ✅ DATABASE_URL (환경변수)"));
|
|
1434
|
-
if (!readSemoEnvDbUrl()) {
|
|
1435
|
-
writeSemoEnvFile(process.env.DATABASE_URL);
|
|
1436
|
-
console.log(chalk_1.default.gray(` → ~/.semo.env 에 저장됨 (비인터랙티브 환경용)`));
|
|
1437
|
-
}
|
|
1438
|
-
return;
|
|
1439
|
-
}
|
|
1440
|
-
// 2. ~/.semo.env 에 이미 있음
|
|
1441
|
-
const existing = readSemoEnvDbUrl();
|
|
1442
|
-
if (existing) {
|
|
1443
|
-
console.log(chalk_1.default.green(" ✅ DATABASE_URL (~/.semo.env)"));
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
// 3. Private GitHub Gist 자동 fetch
|
|
1263
|
+
async function setupSemoEnv(credentialsGist, force) {
|
|
1264
|
+
console.log(chalk_1.default.cyan("\n🔑 환경변수 설정"));
|
|
1265
|
+
// 1. 기존 파일
|
|
1266
|
+
const existing = force ? {} : readSemoEnvCreds();
|
|
1267
|
+
// 2. Gist
|
|
1447
1268
|
const gistId = credentialsGist || process.env.SEMO_CREDENTIALS_GIST;
|
|
1269
|
+
let gistCreds = {};
|
|
1448
1270
|
if (gistId) {
|
|
1449
1271
|
console.log(chalk_1.default.gray(" GitHub Gist에서 팀 접속정보 가져오는 중..."));
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1272
|
+
gistCreds = fetchCredsFromGist(gistId) || {};
|
|
1273
|
+
}
|
|
1274
|
+
// 3. 머지: gist(base) ← file(override) ← env(override)
|
|
1275
|
+
const merged = { ...gistCreds };
|
|
1276
|
+
for (const [k, v] of Object.entries(existing)) {
|
|
1277
|
+
if (v)
|
|
1278
|
+
merged[k] = v;
|
|
1279
|
+
}
|
|
1280
|
+
for (const def of SEMO_CREDENTIALS) {
|
|
1281
|
+
if (process.env[def.key])
|
|
1282
|
+
merged[def.key] = process.env[def.key];
|
|
1283
|
+
}
|
|
1284
|
+
// 4. required인데 없는 키만 프롬프트
|
|
1285
|
+
let hasNewKeys = false;
|
|
1286
|
+
for (const def of SEMO_CREDENTIALS) {
|
|
1287
|
+
if (merged[def.key]) {
|
|
1288
|
+
const label = def.sensitive ? "(설정됨)" : merged[def.key];
|
|
1289
|
+
console.log(chalk_1.default.green(` ✅ ${def.key} ${label}`));
|
|
1290
|
+
}
|
|
1291
|
+
else if (def.required) {
|
|
1292
|
+
const { value } = await inquirer_1.default.prompt([
|
|
1293
|
+
{
|
|
1294
|
+
type: "password",
|
|
1295
|
+
name: "value",
|
|
1296
|
+
message: def.promptMessage,
|
|
1297
|
+
mask: "*",
|
|
1298
|
+
},
|
|
1299
|
+
]);
|
|
1300
|
+
if (value?.trim()) {
|
|
1301
|
+
merged[def.key] = value.trim();
|
|
1302
|
+
hasNewKeys = true;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
console.log(chalk_1.default.gray(` ⏭ ${def.key} (없음 — 선택사항)`));
|
|
1455
1307
|
}
|
|
1456
|
-
console.log(chalk_1.default.yellow(" ⚠️ Gist fetch 실패 — 수동 입력으로 전환"));
|
|
1457
1308
|
}
|
|
1458
|
-
//
|
|
1459
|
-
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
},
|
|
1467
|
-
]);
|
|
1468
|
-
if (dbUrl && dbUrl.trim()) {
|
|
1469
|
-
writeSemoEnvFile(dbUrl.trim());
|
|
1309
|
+
// 5. 변경사항이 있거나 파일이 없으면 쓰기
|
|
1310
|
+
const envFile = path.join(os.homedir(), ".semo.env");
|
|
1311
|
+
const needsWrite = force ||
|
|
1312
|
+
hasNewKeys ||
|
|
1313
|
+
!fs.existsSync(envFile) ||
|
|
1314
|
+
Object.keys(gistCreds).some((k) => !existing[k]);
|
|
1315
|
+
if (needsWrite) {
|
|
1316
|
+
writeSemoEnvFile(merged);
|
|
1470
1317
|
console.log(chalk_1.default.green(" ✅ ~/.semo.env 저장됨 (권한: 600)"));
|
|
1471
|
-
console.log(chalk_1.default.gray(` 파일: ${envFile}`));
|
|
1472
1318
|
}
|
|
1473
1319
|
else {
|
|
1474
|
-
|
|
1475
|
-
if (!fs.existsSync(envFile)) {
|
|
1476
|
-
writeSemoEnvFile("");
|
|
1477
|
-
}
|
|
1478
|
-
console.log(chalk_1.default.yellow(" ⚠️ 건너뜀 — ~/.semo.env에서 DATABASE_URL을 직접 입력하세요"));
|
|
1320
|
+
console.log(chalk_1.default.gray(" ~/.semo.env 변경 없음"));
|
|
1479
1321
|
}
|
|
1480
1322
|
}
|
|
1481
1323
|
// === Claude MCP 서버 존재 여부 확인 ===
|
|
@@ -1504,6 +1346,9 @@ function registerMCPServer(server) {
|
|
|
1504
1346
|
args.push("-e", `${key}=${value}`);
|
|
1505
1347
|
}
|
|
1506
1348
|
}
|
|
1349
|
+
// scope 지정 (기본: project)
|
|
1350
|
+
const scope = server.scope || "project";
|
|
1351
|
+
args.push("-s", scope);
|
|
1507
1352
|
// -- 구분자 후 명령어와 인자 추가
|
|
1508
1353
|
args.push("--", server.command, ...server.args);
|
|
1509
1354
|
(0, child_process_1.execSync)(`claude ${args.join(" ")}`, { stdio: "pipe" });
|
|
@@ -1513,6 +1358,96 @@ function registerMCPServer(server) {
|
|
|
1513
1358
|
return { success: false, error: String(error) };
|
|
1514
1359
|
}
|
|
1515
1360
|
}
|
|
1361
|
+
// === 글로벌 CLAUDE.md에 KB-First 규칙 주입 ===
|
|
1362
|
+
const KB_FIRST_SECTION_MARKER = "## SEMO KB-First 행동 규칙";
|
|
1363
|
+
async function injectKbFirstToGlobalClaudeMd() {
|
|
1364
|
+
const globalClaudeMd = path.join(os.homedir(), ".claude", "CLAUDE.md");
|
|
1365
|
+
const kbFirstBlock = `
|
|
1366
|
+
${KB_FIRST_SECTION_MARKER}
|
|
1367
|
+
|
|
1368
|
+
> semo-kb MCP 서버가 유저레벨에 등록되어 있습니다. KB는 팀의 Single Source of Truth입니다.
|
|
1369
|
+
|
|
1370
|
+
### 읽기 (Query-First)
|
|
1371
|
+
다음 주제 질문 → **반드시 kb_search/kb_get으로 KB 먼저 조회** 후 답변:
|
|
1372
|
+
- 팀원 정보 → \`domain: team\`
|
|
1373
|
+
- 프로젝트 현황 → \`domain: project\`
|
|
1374
|
+
- 의사결정 기록 → \`domain: decision\`
|
|
1375
|
+
- 업무 프로세스 → \`domain: process\`
|
|
1376
|
+
- 인프라 구성 → \`domain: infra\`
|
|
1377
|
+
- KPI → \`domain: kpi\`
|
|
1378
|
+
|
|
1379
|
+
**금지:** 위 주제를 자체 지식/세션 기억만으로 답변하는 것.
|
|
1380
|
+
KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
|
|
1381
|
+
|
|
1382
|
+
### 쓰기 (Write-Back)
|
|
1383
|
+
사용자가 팀 정보를 정정/추가/변경하면, 의사결정이 내려지면 → **반드시 kb_upsert로 KB에 즉시 기록.**
|
|
1384
|
+
**금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
|
|
1385
|
+
`;
|
|
1386
|
+
if (fs.existsSync(globalClaudeMd)) {
|
|
1387
|
+
const content = fs.readFileSync(globalClaudeMd, "utf-8");
|
|
1388
|
+
if (content.includes(KB_FIRST_SECTION_MARKER)) {
|
|
1389
|
+
// 기존 섹션 교체
|
|
1390
|
+
const regex = new RegExp(`\\n${KB_FIRST_SECTION_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?(?=\\n## |$)`, "m");
|
|
1391
|
+
const updated = content.replace(regex, kbFirstBlock);
|
|
1392
|
+
fs.writeFileSync(globalClaudeMd, updated);
|
|
1393
|
+
console.log(chalk_1.default.gray(" ~/.claude/CLAUDE.md KB-First 규칙 업데이트됨"));
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
// 끝에 추가
|
|
1397
|
+
fs.writeFileSync(globalClaudeMd, content.trimEnd() + "\n" + kbFirstBlock);
|
|
1398
|
+
console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md에 KB-First 규칙 추가됨"));
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
else {
|
|
1402
|
+
// 파일 없으면 생성
|
|
1403
|
+
fs.writeFileSync(globalClaudeMd, kbFirstBlock.trim() + "\n");
|
|
1404
|
+
console.log(chalk_1.default.green(" ✓ ~/.claude/CLAUDE.md 생성됨 (KB-First 규칙)"));
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
// === semo-kb MCP 유저레벨 등록 ===
|
|
1408
|
+
async function setupSemoKbMcp() {
|
|
1409
|
+
console.log(chalk_1.default.cyan("\n📡 semo-kb MCP 유저레벨 등록"));
|
|
1410
|
+
console.log(chalk_1.default.gray(" KB-First SoT — 어디서든 KB 조회/갱신 가능\n"));
|
|
1411
|
+
// semo-kb MCP 서버 경로 탐색
|
|
1412
|
+
// 1. cwd에서 packages/mcp-kb/dist/index.js 찾기
|
|
1413
|
+
// 2. CLI 패키지 기준으로 monorepo 루트 탐색
|
|
1414
|
+
// 3. 환경변수 SEMO_PROJECT_ROOT
|
|
1415
|
+
const candidates = [
|
|
1416
|
+
path.join(process.cwd(), "packages", "mcp-kb", "dist", "index.js"),
|
|
1417
|
+
process.env.SEMO_PROJECT_ROOT
|
|
1418
|
+
? path.join(process.env.SEMO_PROJECT_ROOT, "packages", "mcp-kb", "dist", "index.js")
|
|
1419
|
+
: "",
|
|
1420
|
+
path.resolve(__dirname, "..", "..", "..", "mcp-kb", "dist", "index.js"),
|
|
1421
|
+
].filter(Boolean);
|
|
1422
|
+
const mcpEntryPath = candidates.find((p) => fs.existsSync(p));
|
|
1423
|
+
if (!mcpEntryPath) {
|
|
1424
|
+
console.log(chalk_1.default.yellow(" ⚠ semo-kb MCP 서버를 찾을 수 없습니다."));
|
|
1425
|
+
console.log(chalk_1.default.gray(" semo 프로젝트 루트에서 실행하거나 SEMO_PROJECT_ROOT 환경변수를 설정하세요."));
|
|
1426
|
+
console.log(chalk_1.default.gray(" 예: cd /path/to/semo && semo onboarding"));
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const absolutePath = path.resolve(mcpEntryPath);
|
|
1430
|
+
console.log(chalk_1.default.gray(` 경로: ${absolutePath}`));
|
|
1431
|
+
// claude mcp add로 유저레벨 등록
|
|
1432
|
+
const result = registerMCPServer({
|
|
1433
|
+
name: "semo-kb",
|
|
1434
|
+
command: "node",
|
|
1435
|
+
args: [absolutePath],
|
|
1436
|
+
scope: "user",
|
|
1437
|
+
});
|
|
1438
|
+
if (result.success) {
|
|
1439
|
+
if (result.skipped) {
|
|
1440
|
+
console.log(chalk_1.default.gray(" semo-kb 이미 등록됨 (건너뜀)"));
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
console.log(chalk_1.default.green(" ✓ semo-kb MCP 유저레벨 등록 완료"));
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
console.log(chalk_1.default.yellow(` ⚠ semo-kb 등록 실패: ${result.error}`));
|
|
1448
|
+
console.log(chalk_1.default.gray(" 수동 등록: claude mcp add semo-kb -s user -- node " + absolutePath));
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1516
1451
|
// === MCP 설정 ===
|
|
1517
1452
|
async function setupMCP(cwd, _extensions, force) {
|
|
1518
1453
|
console.log(chalk_1.default.cyan("\n🔧 Black Box 설정 (MCP Server)"));
|
|
@@ -1529,23 +1464,13 @@ async function setupMCP(cwd, _extensions, force) {
|
|
|
1529
1464
|
const settings = {
|
|
1530
1465
|
mcpServers: {},
|
|
1531
1466
|
};
|
|
1532
|
-
//
|
|
1533
|
-
|
|
1534
|
-
// settings.json에 mcpServers 저장 (백업용)
|
|
1535
|
-
for (const server of allServers) {
|
|
1536
|
-
const serverConfig = {
|
|
1537
|
-
command: server.command,
|
|
1538
|
-
args: server.args,
|
|
1539
|
-
};
|
|
1540
|
-
if (server.env) {
|
|
1541
|
-
serverConfig.env = server.env;
|
|
1542
|
-
}
|
|
1543
|
-
settings.mcpServers[server.name] = serverConfig;
|
|
1544
|
-
}
|
|
1467
|
+
// semo-kb는 유저레벨에서 등록 (semo onboarding)하므로 프로젝트 settings에 쓰지 않음
|
|
1468
|
+
// 공통 서버(context7 등)도 유저레벨에 등록하므로 프로젝트 settings에 쓰지 않음
|
|
1545
1469
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
1546
|
-
console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨
|
|
1547
|
-
// Claude Code에 MCP 서버 등록 시도
|
|
1470
|
+
console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨"));
|
|
1471
|
+
// Claude Code에 MCP 서버 등록 시도 (공통 서버는 유저레벨로)
|
|
1548
1472
|
console.log(chalk_1.default.cyan("\n🔌 Claude Code에 MCP 서버 등록 중..."));
|
|
1473
|
+
const allServers = [...BASE_MCP_SERVERS];
|
|
1549
1474
|
const successServers = [];
|
|
1550
1475
|
const skippedServers = [];
|
|
1551
1476
|
const failedServers = [];
|
|
@@ -1628,12 +1553,13 @@ semo-system/
|
|
|
1628
1553
|
}
|
|
1629
1554
|
}
|
|
1630
1555
|
// === Hooks 설치/업데이트 ===
|
|
1631
|
-
async function setupHooks(
|
|
1556
|
+
async function setupHooks(isUpdate = false) {
|
|
1632
1557
|
const action = isUpdate ? "업데이트" : "설치";
|
|
1633
1558
|
console.log(chalk_1.default.cyan(`\n🪝 Claude Code Hooks ${action}`));
|
|
1634
|
-
|
|
1559
|
+
console.log(chalk_1.default.gray(" semo CLI 기반 컨텍스트 동기화\n"));
|
|
1560
|
+
const homeDir = os.homedir();
|
|
1635
1561
|
const settingsPath = path.join(homeDir, ".claude", "settings.local.json");
|
|
1636
|
-
//
|
|
1562
|
+
// hooks 설정 객체 — semo CLI만 사용 (프로젝트 경로 무관)
|
|
1637
1563
|
const hooksConfig = {
|
|
1638
1564
|
SessionStart: [
|
|
1639
1565
|
{
|
|
@@ -1660,36 +1586,6 @@ async function setupHooks(cwd, isUpdate = false) {
|
|
|
1660
1586
|
},
|
|
1661
1587
|
],
|
|
1662
1588
|
};
|
|
1663
|
-
// semo-hooks 빌드 (선택적 — semo-system 레포에서만 동작)
|
|
1664
|
-
const hooksDir = path.join(cwd, "semo-system", "semo-hooks");
|
|
1665
|
-
if (fs.existsSync(hooksDir)) {
|
|
1666
|
-
let hooksBuilt = false;
|
|
1667
|
-
try {
|
|
1668
|
-
(0, child_process_1.execSync)("npm install", { cwd: hooksDir, stdio: ["pipe", "pipe", "pipe"] });
|
|
1669
|
-
(0, child_process_1.execSync)("npm run build", { cwd: hooksDir, stdio: ["pipe", "pipe", "pipe"] });
|
|
1670
|
-
hooksBuilt = true;
|
|
1671
|
-
}
|
|
1672
|
-
catch {
|
|
1673
|
-
console.log(chalk_1.default.yellow(" ⚠ semo-hooks 빌드 실패 (core 훅만 등록)"));
|
|
1674
|
-
}
|
|
1675
|
-
if (hooksBuilt) {
|
|
1676
|
-
const hooksCmd = `node ${path.join(hooksDir, "dist", "index.js")}`;
|
|
1677
|
-
hooksConfig.SessionStart.unshift({
|
|
1678
|
-
matcher: "",
|
|
1679
|
-
hooks: [{ type: "command", command: `${hooksCmd} session-start`, timeout: 10 }],
|
|
1680
|
-
});
|
|
1681
|
-
hooksConfig.UserPromptSubmit = [
|
|
1682
|
-
{ matcher: "", hooks: [{ type: "command", command: `${hooksCmd} user-prompt`, timeout: 5 }] },
|
|
1683
|
-
];
|
|
1684
|
-
hooksConfig.Stop.unshift({
|
|
1685
|
-
matcher: "",
|
|
1686
|
-
hooks: [{ type: "command", command: `${hooksCmd} stop`, timeout: 10 }],
|
|
1687
|
-
});
|
|
1688
|
-
hooksConfig.SessionEnd = [
|
|
1689
|
-
{ matcher: "", hooks: [{ type: "command", command: `${hooksCmd} session-end`, timeout: 10 }] },
|
|
1690
|
-
];
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
1589
|
// 기존 설정 로드 또는 새로 생성
|
|
1694
1590
|
let existingSettings = {};
|
|
1695
1591
|
const claudeConfigDir = path.join(homeDir, ".claude");
|
|
@@ -1890,132 +1786,86 @@ async function setupClaudeMd(cwd, _extensions, force) {
|
|
|
1890
1786
|
return;
|
|
1891
1787
|
}
|
|
1892
1788
|
}
|
|
1893
|
-
|
|
1894
|
-
const
|
|
1895
|
-
const claudeMdContent = `# ${projectName} — Claude Configuration
|
|
1896
|
-
|
|
1897
|
-
> SEMO v${VERSION} 설치됨 (${installDate})
|
|
1898
|
-
|
|
1899
|
-
---
|
|
1900
|
-
|
|
1901
|
-
## SEMO란?
|
|
1902
|
-
|
|
1903
|
-
**SEMO (Semicolon Orchestrate)** 는 OpenClaw 봇팀과 로컬 Claude Code 세션이
|
|
1904
|
-
**팀 Core DB를 단일 진실 공급원(Single Source of Truth)으로 공유**하는 컨텍스트 동기화 시스템이다.
|
|
1905
|
-
|
|
1906
|
-
\`\`\`
|
|
1907
|
-
로컬 Claude Code 세션
|
|
1908
|
-
↕ semo context sync / push
|
|
1909
|
-
팀 Core DB (PostgreSQL, semo 스키마)
|
|
1910
|
-
↕ 봇 세션 시작/종료 훅
|
|
1911
|
-
OpenClaw 봇팀 (7개 봇)
|
|
1912
|
-
workclaw · reviewclaw · planclaw · designclaw
|
|
1913
|
-
infraclaw · growthclaw · semiclaw
|
|
1914
|
-
\`\`\`
|
|
1915
|
-
|
|
1916
|
-
**이 CLAUDE.md가 설치된 프로젝트는 OpenClaw 봇팀의 컨텍스트를 실시간으로 공유받는다.**
|
|
1917
|
-
|
|
1918
|
-
---
|
|
1919
|
-
|
|
1920
|
-
## 자동 동기화
|
|
1921
|
-
|
|
1922
|
-
세션 시작/종료 시 팀 Core DB와 자동 동기화됩니다.
|
|
1789
|
+
// 프로젝트 규칙만 (스킬/에이전트 목록은 글로벌 ~/.claude/에 있음)
|
|
1790
|
+
const claudeMdContent = `# SEMO Project Configuration
|
|
1923
1791
|
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
| 세션 시작 | \`semo context sync\` → \`.claude/memory/\` 최신화 |
|
|
1927
|
-
| 세션 종료 | \`semo context push\` → \`decisions.md\` 변경분 DB 저장 |
|
|
1792
|
+
> SEMO (Semicolon Orchestrate) - AI Agent Orchestration Framework v${VERSION}
|
|
1793
|
+
> 스킬/에이전트/커맨드는 글로벌(~/.claude/)에서 로드됩니다.
|
|
1928
1794
|
|
|
1929
1795
|
---
|
|
1930
1796
|
|
|
1931
|
-
##
|
|
1932
|
-
|
|
1933
|
-
\`.claude/memory/\` 파일들은 **팀 Core DB (\`semo\` 스키마)에서 자동으로 채워집니다**.
|
|
1934
|
-
직접 편집하지 마세요 — 세션 시작 시 덮어씌워집니다.
|
|
1797
|
+
## KB 접근 (semo-kb MCP 서버)
|
|
1935
1798
|
|
|
1936
|
-
|
|
1937
|
-
|------|---------|------|
|
|
1938
|
-
| \`team.md\` | \`kb WHERE domain='team'\` | DB → 로컬 (읽기 전용) |
|
|
1939
|
-
| \`projects.md\` | \`kb WHERE domain='project'\` | DB → 로컬 (읽기 전용) |
|
|
1940
|
-
| \`decisions.md\` | \`kb WHERE domain='decision'\` | **양방향** (편집 가능, Stop 시 DB 저장) |
|
|
1941
|
-
| \`infra.md\` | \`kb WHERE domain='infra'\` | DB → 로컬 (읽기 전용) |
|
|
1942
|
-
| \`process.md\` | \`kb WHERE domain='process'\` | DB → 로컬 (읽기 전용) |
|
|
1943
|
-
| \`bots.md\` | \`semo.bot_status\` | DB → 로컬 (봇 상태) |
|
|
1944
|
-
| \`ontology.md\` | \`semo.ontology\` | DB → 로컬 (읽기 전용) |
|
|
1799
|
+
KB 데이터는 **semo-kb MCP 서버**를 통해 Core DB에서 실시간 조회합니다.
|
|
1945
1800
|
|
|
1946
|
-
|
|
1801
|
+
| MCP 도구 | 설명 |
|
|
1802
|
+
|----------|------|
|
|
1803
|
+
| \`kb_search\` | 벡터+텍스트 하이브리드 검색 (query, domain?, limit?, mode?) |
|
|
1804
|
+
| \`kb_get\` | domain+key 정확 조회 |
|
|
1805
|
+
| \`kb_list\` | 도메인별 엔트리 목록 |
|
|
1806
|
+
| \`kb_upsert\` | KB 항목 쓰기 (OpenAI 임베딩 자동 생성) |
|
|
1807
|
+
| \`kb_bot_status\` | 봇 상태 테이블 조회 |
|
|
1808
|
+
| \`kb_ontology\` | 온톨로지 스키마 조회 |
|
|
1809
|
+
| \`kb_digest\` | 봇 구독 도메인 변경 다이제스트 |
|
|
1947
1810
|
|
|
1948
1811
|
---
|
|
1949
1812
|
|
|
1950
|
-
##
|
|
1951
|
-
|
|
1952
|
-
\`\`\`
|
|
1953
|
-
.claude/
|
|
1954
|
-
├── CLAUDE.md # 이 파일
|
|
1955
|
-
├── settings.json # MCP 서버 설정 + SessionStart/Stop 훅
|
|
1956
|
-
├── memory/ # Core DB → 로컬 자동 동기화 컨텍스트
|
|
1957
|
-
├── skills/ # SEMO 스킬 (semo-system/semo-skills/ 링크)
|
|
1958
|
-
├── agents/ # SEMO 에이전트 (semo-system/meta/agents/ 링크)
|
|
1959
|
-
└── commands/SEMO # 슬래시 커맨드 (semo-system/semo-core/commands/)
|
|
1960
|
-
\`\`\`
|
|
1961
|
-
|
|
1962
|
-
---
|
|
1963
|
-
|
|
1964
|
-
## 프로젝트 규칙 (팀이 채워야 함)
|
|
1965
|
-
|
|
1966
|
-
> 아래 섹션은 이 프로젝트 고유의 규칙을 기록하세요.
|
|
1967
|
-
> 팀 공통 규칙은 \`memory/process.md\`에 있습니다.
|
|
1968
|
-
|
|
1969
|
-
### 기술 스택
|
|
1970
|
-
|
|
1971
|
-
<!-- 예: Next.js 14, PostgreSQL, TypeScript strict mode -->
|
|
1813
|
+
## KB-First 행동 규칙 (NON-NEGOTIABLE)
|
|
1972
1814
|
|
|
1973
|
-
|
|
1815
|
+
> KB는 팀의 Single Source of Truth이다. 아래 규칙은 예외 없이 적용된다.
|
|
1974
1816
|
|
|
1975
|
-
|
|
1817
|
+
### 읽기 (Query-First)
|
|
1818
|
+
다음 주제 질문 → **반드시 kb_search/kb_get으로 KB 먼저 조회** 후 답변:
|
|
1819
|
+
- 팀원 정보 → \`domain: team\`
|
|
1820
|
+
- 프로젝트 현황 → \`domain: project\`
|
|
1821
|
+
- 의사결정 기록 → \`domain: decision\`
|
|
1822
|
+
- 업무 프로세스 → \`domain: process\`
|
|
1823
|
+
- 인프라 구성 → \`domain: infra\`
|
|
1824
|
+
- KPI → \`domain: kpi\`
|
|
1976
1825
|
|
|
1977
|
-
|
|
1826
|
+
**금지:** 위 주제를 자체 지식/세션 기억만으로 답변하는 것.
|
|
1827
|
+
KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
|
|
1978
1828
|
|
|
1979
|
-
|
|
1829
|
+
### 쓰기 (Write-Back)
|
|
1830
|
+
다음 상황 → **반드시 kb_upsert로 KB에 즉시 기록:**
|
|
1831
|
+
- 사용자가 팀 정보를 정정하거나 새 사실을 알려줄 때
|
|
1832
|
+
- 의사결정이 내려졌을 때
|
|
1833
|
+
- 프로세스/규칙이 변경되었을 때
|
|
1980
1834
|
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
<!-- 예: DB 직접 접근 금지 — 반드시 API route 통해야 함 -->
|
|
1835
|
+
**금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
|
|
1984
1836
|
|
|
1985
1837
|
---
|
|
1986
1838
|
|
|
1987
|
-
## Quality Gate
|
|
1839
|
+
## Pre-Commit Quality Gate
|
|
1988
1840
|
|
|
1989
|
-
|
|
1841
|
+
> **코드 변경이 포함된 커밋 전 반드시 Quality Gate를 통과해야 합니다.**
|
|
1990
1842
|
|
|
1991
1843
|
\`\`\`bash
|
|
1992
|
-
npm run lint
|
|
1993
|
-
npx tsc --noEmit
|
|
1994
|
-
npm run build
|
|
1844
|
+
npm run lint # ESLint 검사
|
|
1845
|
+
npx tsc --noEmit # TypeScript 타입 체크
|
|
1846
|
+
npm run build # 빌드 검증
|
|
1995
1847
|
\`\`\`
|
|
1996
1848
|
|
|
1997
|
-
\`--no-verify\` 사용 금지.
|
|
1849
|
+
\`--no-verify\` 플래그 사용 금지.
|
|
1998
1850
|
|
|
1999
1851
|
---
|
|
2000
1852
|
|
|
2001
|
-
##
|
|
1853
|
+
## 환경변수 (\`~/.semo.env\`)
|
|
2002
1854
|
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
| \`/SEMO:help\` | 도움말 |
|
|
2006
|
-
| \`/SEMO:feedback\` | 피드백 제출 |
|
|
2007
|
-
| \`/SEMO:health\` | 환경 헬스체크 |
|
|
1855
|
+
SEMO는 \`~/.semo.env\` 파일에서 팀 공통 환경변수를 로드합니다.
|
|
1856
|
+
SessionStart 훅과 OpenClaw 게이트웨이 래퍼에서 자동 source됩니다.
|
|
2008
1857
|
|
|
2009
|
-
|
|
1858
|
+
| 변수 | 용도 | 필수 |
|
|
1859
|
+
|------|------|------|
|
|
1860
|
+
| \`DATABASE_URL\` | 팀 Core DB (PostgreSQL) 연결 | ✅ |
|
|
1861
|
+
| \`OPENAI_API_KEY\` | KB 임베딩용 (text-embedding-3-small) | 선택 |
|
|
1862
|
+
| \`SLACK_WEBHOOK\` | Slack 알림 | 선택 |
|
|
2010
1863
|
|
|
2011
|
-
|
|
1864
|
+
키 갱신이 필요하면 \`~/.semo.env\`를 직접 편집하거나 \`semo onboarding -f\`를 실행하세요.
|
|
2012
1865
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
semo context sync # memory/ 수동 최신화
|
|
2017
|
-
semo bots status # 봇 상태 조회
|
|
2018
|
-
\`\`\`
|
|
1866
|
+
---
|
|
1867
|
+
|
|
1868
|
+
> Generated by SEMO CLI v${VERSION}
|
|
2019
1869
|
`;
|
|
2020
1870
|
fs.writeFileSync(claudeMdPath, claudeMdContent);
|
|
2021
1871
|
console.log(chalk_1.default.green("✓ .claude/CLAUDE.md 생성됨"));
|
|
@@ -2030,7 +1880,7 @@ program
|
|
|
2030
1880
|
console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록\n"));
|
|
2031
1881
|
// Standard 패키지 표시
|
|
2032
1882
|
console.log(chalk_1.default.white.bold("Standard (필수)"));
|
|
2033
|
-
const standardPkgs = ["semo-core", "semo-
|
|
1883
|
+
const standardPkgs = ["semo-core", "semo-agents", "semo-scripts"];
|
|
2034
1884
|
for (const pkg of standardPkgs) {
|
|
2035
1885
|
const isInstalled = fs.existsSync(path.join(semoSystemDir, pkg));
|
|
2036
1886
|
console.log(` ${isInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} ${pkg}`);
|
|
@@ -2063,7 +1913,6 @@ program
|
|
|
2063
1913
|
console.log(chalk_1.default.white.bold("Standard:"));
|
|
2064
1914
|
const standardChecks = [
|
|
2065
1915
|
{ name: "semo-core", path: path.join(semoSystemDir, "semo-core") },
|
|
2066
|
-
{ name: "semo-skills", path: path.join(semoSystemDir, "semo-skills") },
|
|
2067
1916
|
];
|
|
2068
1917
|
let standardOk = true;
|
|
2069
1918
|
for (const check of standardChecks) {
|
|
@@ -2101,11 +1950,27 @@ program
|
|
|
2101
1950
|
.command("update")
|
|
2102
1951
|
.description("SEMO를 최신 버전으로 업데이트합니다")
|
|
2103
1952
|
.option("--self", "CLI만 업데이트")
|
|
1953
|
+
.option("--global", "글로벌 스킬/커맨드/에이전트를 DB 최신으로 갱신 (~/.claude/)")
|
|
2104
1954
|
.option("--system", "semo-system만 업데이트")
|
|
2105
1955
|
.option("--skip-cli", "CLI 업데이트 건너뛰기")
|
|
2106
|
-
.option("--only <packages>", "특정 패키지만 업데이트 (쉼표 구분: semo-core,
|
|
1956
|
+
.option("--only <packages>", "특정 패키지만 업데이트 (쉼표 구분: semo-core,biz/management)")
|
|
2107
1957
|
.option("--migrate", "레거시 환경 강제 마이그레이션")
|
|
2108
1958
|
.action(async (options) => {
|
|
1959
|
+
// === --global: 글로벌 스킬 갱신 ===
|
|
1960
|
+
if (options.global) {
|
|
1961
|
+
console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 글로벌 업데이트\n"));
|
|
1962
|
+
console.log(chalk_1.default.gray(" 대상: ~/.claude/skills, commands, agents (DB 최신)\n"));
|
|
1963
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
1964
|
+
if (!connected) {
|
|
1965
|
+
console.log(chalk_1.default.red(" DB 연결 실패 — ~/.semo.env를 확인하세요."));
|
|
1966
|
+
await (0, database_1.closeConnection)();
|
|
1967
|
+
process.exit(1);
|
|
1968
|
+
}
|
|
1969
|
+
await setupStandardGlobal();
|
|
1970
|
+
await (0, database_1.closeConnection)();
|
|
1971
|
+
console.log(chalk_1.default.green.bold("\n✅ 글로벌 스킬 업데이트 완료!\n"));
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
2109
1974
|
console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 업데이트\n"));
|
|
2110
1975
|
const cwd = process.cwd();
|
|
2111
1976
|
const semoSystemDir = path.join(cwd, "semo-system");
|
|
@@ -2159,22 +2024,19 @@ program
|
|
|
2159
2024
|
}
|
|
2160
2025
|
// 업데이트 대상 결정
|
|
2161
2026
|
const updateSemoCore = !isSelectiveUpdate || onlyPackages.includes("semo-core");
|
|
2162
|
-
const updateSemoSkills = !isSelectiveUpdate || onlyPackages.includes("semo-skills");
|
|
2163
2027
|
const updateSemoAgents = !isSelectiveUpdate || onlyPackages.includes("semo-agents");
|
|
2164
2028
|
const updateSemoScripts = !isSelectiveUpdate || onlyPackages.includes("semo-scripts");
|
|
2165
2029
|
console.log(chalk_1.default.cyan("\n📚 semo-system 업데이트"));
|
|
2166
2030
|
console.log(chalk_1.default.gray(" 대상:"));
|
|
2167
2031
|
if (updateSemoCore)
|
|
2168
2032
|
console.log(chalk_1.default.gray(" - semo-core"));
|
|
2169
|
-
if (updateSemoSkills)
|
|
2170
|
-
console.log(chalk_1.default.gray(" - semo-skills"));
|
|
2171
2033
|
if (updateSemoAgents)
|
|
2172
2034
|
console.log(chalk_1.default.gray(" - semo-agents"));
|
|
2173
2035
|
if (updateSemoScripts)
|
|
2174
2036
|
console.log(chalk_1.default.gray(" - semo-scripts"));
|
|
2175
|
-
if (!updateSemoCore && !
|
|
2037
|
+
if (!updateSemoCore && !updateSemoAgents && !updateSemoScripts) {
|
|
2176
2038
|
console.log(chalk_1.default.yellow("\n ⚠️ 업데이트할 패키지가 없습니다."));
|
|
2177
|
-
console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-
|
|
2039
|
+
console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-agents, semo-scripts"));
|
|
2178
2040
|
return;
|
|
2179
2041
|
}
|
|
2180
2042
|
const spinner = (0, ora_1.default)("\n 최신 버전 다운로드 중...").start();
|
|
@@ -2185,7 +2047,6 @@ program
|
|
|
2185
2047
|
// Standard 업데이트 (선택적) - semo-system/ 하위에서 복사
|
|
2186
2048
|
const standardUpdates = [
|
|
2187
2049
|
{ flag: updateSemoCore, name: "semo-core" },
|
|
2188
|
-
{ flag: updateSemoSkills, name: "semo-skills" },
|
|
2189
2050
|
{ flag: updateSemoAgents, name: "semo-agents" },
|
|
2190
2051
|
{ flag: updateSemoScripts, name: "semo-scripts" },
|
|
2191
2052
|
];
|
|
@@ -2272,7 +2133,7 @@ program
|
|
|
2272
2133
|
}
|
|
2273
2134
|
}
|
|
2274
2135
|
// === 6. Hooks 업데이트 ===
|
|
2275
|
-
await setupHooks(
|
|
2136
|
+
await setupHooks(true);
|
|
2276
2137
|
// === 7. 설치 검증 ===
|
|
2277
2138
|
const verificationResult = verifyInstallation(cwd, []);
|
|
2278
2139
|
printVerificationResult(verificationResult);
|
|
@@ -2331,20 +2192,22 @@ program
|
|
|
2331
2192
|
// === config 명령어 (설치 후 설정 변경) ===
|
|
2332
2193
|
const configCmd = program.command("config").description("SEMO 설정 관리");
|
|
2333
2194
|
configCmd
|
|
2334
|
-
.command("
|
|
2335
|
-
.description("
|
|
2195
|
+
.command("env")
|
|
2196
|
+
.description("SEMO 환경변수 설정 (DATABASE_URL, OPENAI_API_KEY 등)")
|
|
2336
2197
|
.option("--credentials-gist <gistId>", "Private GitHub Gist에서 자동 가져오기")
|
|
2198
|
+
.option("--force", "기존 값도 Gist에서 덮어쓰기")
|
|
2337
2199
|
.action(async (options) => {
|
|
2338
|
-
console.log(chalk_1.default.cyan.bold("\n🔑
|
|
2339
|
-
|
|
2340
|
-
const
|
|
2341
|
-
if (
|
|
2200
|
+
console.log(chalk_1.default.cyan.bold("\n🔑 SEMO 환경변수 설정\n"));
|
|
2201
|
+
const existing = readSemoEnvCreds();
|
|
2202
|
+
const hasExisting = Object.keys(existing).length > 0;
|
|
2203
|
+
if (hasExisting && !options.force) {
|
|
2204
|
+
const keys = Object.keys(existing).join(", ");
|
|
2342
2205
|
const { overwrite } = await inquirer_1.default.prompt([
|
|
2343
2206
|
{
|
|
2344
2207
|
type: "confirm",
|
|
2345
2208
|
name: "overwrite",
|
|
2346
|
-
message: `기존
|
|
2347
|
-
default:
|
|
2209
|
+
message: `기존 설정이 있습니다 (${keys}). Gist에서 없는 키를 보충하시겠습니까?`,
|
|
2210
|
+
default: true,
|
|
2348
2211
|
},
|
|
2349
2212
|
]);
|
|
2350
2213
|
if (!overwrite) {
|
|
@@ -2352,34 +2215,8 @@ configCmd
|
|
|
2352
2215
|
return;
|
|
2353
2216
|
}
|
|
2354
2217
|
}
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
if (gistId) {
|
|
2358
|
-
console.log(chalk_1.default.gray("GitHub Gist에서 접속정보 가져오는 중..."));
|
|
2359
|
-
const url = fetchDbUrlFromGist(gistId);
|
|
2360
|
-
if (url) {
|
|
2361
|
-
writeSemoEnvFile(url);
|
|
2362
|
-
console.log(chalk_1.default.green("✅ ~/.semo.env 업데이트 완료"));
|
|
2363
|
-
return;
|
|
2364
|
-
}
|
|
2365
|
-
console.log(chalk_1.default.yellow("⚠️ Gist fetch 실패 — 수동 입력"));
|
|
2366
|
-
}
|
|
2367
|
-
const { dbUrl } = await inquirer_1.default.prompt([
|
|
2368
|
-
{
|
|
2369
|
-
type: "password",
|
|
2370
|
-
name: "dbUrl",
|
|
2371
|
-
message: "DATABASE_URL:",
|
|
2372
|
-
mask: "*",
|
|
2373
|
-
},
|
|
2374
|
-
]);
|
|
2375
|
-
if (dbUrl && dbUrl.trim()) {
|
|
2376
|
-
writeSemoEnvFile(dbUrl.trim());
|
|
2377
|
-
console.log(chalk_1.default.green("✅ ~/.semo.env 저장됨 (권한: 600)"));
|
|
2378
|
-
console.log(chalk_1.default.gray(" 다음 Claude Code 세션부터 자동으로 적용됩니다."));
|
|
2379
|
-
}
|
|
2380
|
-
else {
|
|
2381
|
-
console.log(chalk_1.default.yellow("취소됨"));
|
|
2382
|
-
}
|
|
2218
|
+
await setupSemoEnv(options.credentialsGist, options.force);
|
|
2219
|
+
console.log(chalk_1.default.gray(" 다음 Claude Code 세션부터 자동으로 적용됩니다."));
|
|
2383
2220
|
});
|
|
2384
2221
|
// === doctor 명령어 (설치 상태 진단) ===
|
|
2385
2222
|
program
|
|
@@ -2410,7 +2247,7 @@ program
|
|
|
2410
2247
|
console.log(chalk_1.default.gray(" 💡 해결: semo init 실행"));
|
|
2411
2248
|
}
|
|
2412
2249
|
else {
|
|
2413
|
-
const packages = ["semo-core", "semo-
|
|
2250
|
+
const packages = ["semo-core", "semo-agents", "semo-scripts"];
|
|
2414
2251
|
for (const pkg of packages) {
|
|
2415
2252
|
const pkgPath = path.join(semoSystemDir, pkg);
|
|
2416
2253
|
if (fs.existsSync(pkgPath)) {
|
|
@@ -2484,19 +2321,7 @@ function readSyncState(cwd) {
|
|
|
2484
2321
|
}
|
|
2485
2322
|
catch { /* */ }
|
|
2486
2323
|
}
|
|
2487
|
-
return {
|
|
2488
|
-
}
|
|
2489
|
-
function detectBotId() {
|
|
2490
|
-
// 1. Env var
|
|
2491
|
-
if (process.env.SEMO_BOT_ID)
|
|
2492
|
-
return process.env.SEMO_BOT_ID;
|
|
2493
|
-
// 2. Detect from cwd (e.g. ~/.openclaw-workclaw → workclaw)
|
|
2494
|
-
const cwd = process.cwd();
|
|
2495
|
-
const match = cwd.match(/\.openclaw-(\w+)/);
|
|
2496
|
-
if (match)
|
|
2497
|
-
return match[1];
|
|
2498
|
-
// 3. Default
|
|
2499
|
-
return "unknown";
|
|
2324
|
+
return { lastPull: null, lastPush: null, sharedCount: 0 };
|
|
2500
2325
|
}
|
|
2501
2326
|
const kbCmd = program
|
|
2502
2327
|
.command("kb")
|
|
@@ -2504,16 +2329,14 @@ const kbCmd = program
|
|
|
2504
2329
|
kbCmd
|
|
2505
2330
|
.command("pull")
|
|
2506
2331
|
.description("DB에서 KB를 로컬 .kb/로 내려받기")
|
|
2507
|
-
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2508
2332
|
.option("--domain <name>", "특정 도메인만")
|
|
2509
2333
|
.action(async (options) => {
|
|
2510
2334
|
const spinner = (0, ora_1.default)("KB 데이터 가져오는 중...").start();
|
|
2511
2335
|
try {
|
|
2512
2336
|
const pool = (0, database_1.getPool)();
|
|
2513
|
-
const result = await (0, kb_1.kbPull)(pool, options.
|
|
2337
|
+
const result = await (0, kb_1.kbPull)(pool, options.domain, process.cwd());
|
|
2514
2338
|
spinner.succeed(`KB pull 완료`);
|
|
2515
|
-
console.log(chalk_1.default.green(` 📦
|
|
2516
|
-
console.log(chalk_1.default.green(` 🤖 봇 KB (${options.bot}): ${result.bot.length}건`));
|
|
2339
|
+
console.log(chalk_1.default.green(` 📦 KB: ${result.length}건`));
|
|
2517
2340
|
// Also pull ontology
|
|
2518
2341
|
const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
|
|
2519
2342
|
console.log(chalk_1.default.green(` 📐 온톨로지: ${ontoCount}개 도메인`));
|
|
@@ -2529,9 +2352,8 @@ kbCmd
|
|
|
2529
2352
|
kbCmd
|
|
2530
2353
|
.command("push")
|
|
2531
2354
|
.description("로컬 .kb/ 데이터를 DB에 업로드")
|
|
2532
|
-
.option("--
|
|
2533
|
-
.option("--
|
|
2534
|
-
.option("--file <path>", ".kb/ 내 특정 파일")
|
|
2355
|
+
.option("--file <path>", ".kb/ 내 특정 파일", "team.json")
|
|
2356
|
+
.option("--created-by <name>", "작성자 식별자")
|
|
2535
2357
|
.action(async (options) => {
|
|
2536
2358
|
const cwd = process.cwd();
|
|
2537
2359
|
const kbDir = path.join(cwd, ".kb");
|
|
@@ -2542,14 +2364,13 @@ kbCmd
|
|
|
2542
2364
|
const spinner = (0, ora_1.default)("KB 데이터 업로드 중...").start();
|
|
2543
2365
|
try {
|
|
2544
2366
|
const pool = (0, database_1.getPool)();
|
|
2545
|
-
const
|
|
2546
|
-
const filePath = path.join(kbDir, filename);
|
|
2367
|
+
const filePath = path.join(kbDir, options.file);
|
|
2547
2368
|
if (!fs.existsSync(filePath)) {
|
|
2548
|
-
spinner.fail(`파일을 찾을 수 없습니다: .kb/${
|
|
2369
|
+
spinner.fail(`파일을 찾을 수 없습니다: .kb/${options.file}`);
|
|
2549
2370
|
process.exit(1);
|
|
2550
2371
|
}
|
|
2551
2372
|
const entries = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2552
|
-
const result = await (0, kb_1.kbPush)(pool,
|
|
2373
|
+
const result = await (0, kb_1.kbPush)(pool, entries, options.createdBy, cwd);
|
|
2553
2374
|
spinner.succeed(`KB push 완료`);
|
|
2554
2375
|
console.log(chalk_1.default.green(` ✅ ${result.upserted}건 업서트됨`));
|
|
2555
2376
|
if (result.errors.length > 0) {
|
|
@@ -2567,15 +2388,14 @@ kbCmd
|
|
|
2567
2388
|
kbCmd
|
|
2568
2389
|
.command("status")
|
|
2569
2390
|
.description("KB 동기화 상태 확인")
|
|
2570
|
-
.
|
|
2571
|
-
.action(async (options) => {
|
|
2391
|
+
.action(async () => {
|
|
2572
2392
|
const spinner = (0, ora_1.default)("KB 상태 조회 중...").start();
|
|
2573
2393
|
try {
|
|
2574
2394
|
const pool = (0, database_1.getPool)();
|
|
2575
|
-
const status = await (0, kb_1.kbStatus)(pool
|
|
2395
|
+
const status = await (0, kb_1.kbStatus)(pool);
|
|
2576
2396
|
spinner.stop();
|
|
2577
2397
|
console.log(chalk_1.default.cyan.bold("\n📊 KB 상태\n"));
|
|
2578
|
-
console.log(chalk_1.default.white(" 📦
|
|
2398
|
+
console.log(chalk_1.default.white(" 📦 KB (knowledge_base)"));
|
|
2579
2399
|
console.log(chalk_1.default.gray(` 총 ${status.shared.total}건`));
|
|
2580
2400
|
for (const [domain, count] of Object.entries(status.shared.domains)) {
|
|
2581
2401
|
console.log(chalk_1.default.gray(` - ${domain}: ${count}건`));
|
|
@@ -2583,17 +2403,6 @@ kbCmd
|
|
|
2583
2403
|
if (status.shared.lastUpdated) {
|
|
2584
2404
|
console.log(chalk_1.default.gray(` 최종 업데이트: ${status.shared.lastUpdated}`));
|
|
2585
2405
|
}
|
|
2586
|
-
console.log(chalk_1.default.white(`\n 🤖 봇 KB (${options.bot})`));
|
|
2587
|
-
console.log(chalk_1.default.gray(` 총 ${status.bot.total}건`));
|
|
2588
|
-
for (const [domain, count] of Object.entries(status.bot.domains)) {
|
|
2589
|
-
console.log(chalk_1.default.gray(` - ${domain}: ${count}건`));
|
|
2590
|
-
}
|
|
2591
|
-
if (status.bot.lastUpdated) {
|
|
2592
|
-
console.log(chalk_1.default.gray(` 최종 업데이트: ${status.bot.lastUpdated}`));
|
|
2593
|
-
}
|
|
2594
|
-
if (status.bot.lastSynced) {
|
|
2595
|
-
console.log(chalk_1.default.gray(` 최종 동기화: ${status.bot.lastSynced}`));
|
|
2596
|
-
}
|
|
2597
2406
|
// Local sync state
|
|
2598
2407
|
const syncState = readSyncState(process.cwd());
|
|
2599
2408
|
if (syncState.lastPull || syncState.lastPush) {
|
|
@@ -2615,42 +2424,32 @@ kbCmd
|
|
|
2615
2424
|
kbCmd
|
|
2616
2425
|
.command("list")
|
|
2617
2426
|
.description("KB 항목 목록 조회")
|
|
2618
|
-
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2619
2427
|
.option("--domain <name>", "도메인 필터")
|
|
2428
|
+
.option("--service <name>", "서비스(프로젝트) 필터 — 해당 서비스의 모든 도메인 항목 반환")
|
|
2620
2429
|
.option("--limit <n>", "최대 항목 수", "50")
|
|
2621
2430
|
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
2622
2431
|
.action(async (options) => {
|
|
2623
2432
|
try {
|
|
2624
2433
|
const pool = (0, database_1.getPool)();
|
|
2625
|
-
const
|
|
2434
|
+
const entries = await (0, kb_1.kbList)(pool, {
|
|
2626
2435
|
domain: options.domain,
|
|
2627
|
-
|
|
2436
|
+
service: options.service,
|
|
2628
2437
|
limit: parseInt(options.limit),
|
|
2629
2438
|
});
|
|
2630
2439
|
if (options.format === "json") {
|
|
2631
|
-
console.log(JSON.stringify(
|
|
2440
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
2632
2441
|
}
|
|
2633
2442
|
else {
|
|
2634
2443
|
console.log(chalk_1.default.cyan.bold("\n📋 KB 목록\n"));
|
|
2635
|
-
if (
|
|
2636
|
-
console.log(chalk_1.default.white(" 📦 공통 KB"));
|
|
2444
|
+
if (entries.length > 0) {
|
|
2637
2445
|
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
2638
|
-
for (const entry of
|
|
2446
|
+
for (const entry of entries) {
|
|
2639
2447
|
const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
|
|
2640
2448
|
console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
|
|
2641
2449
|
console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
|
|
2642
2450
|
}
|
|
2643
2451
|
}
|
|
2644
|
-
|
|
2645
|
-
console.log(chalk_1.default.white(`\n 🤖 봇 KB (${options.bot})`));
|
|
2646
|
-
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
2647
|
-
for (const entry of result.bot) {
|
|
2648
|
-
const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
|
|
2649
|
-
console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
|
|
2650
|
-
console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
if (result.shared.length === 0 && result.bot.length === 0) {
|
|
2452
|
+
else {
|
|
2654
2453
|
console.log(chalk_1.default.yellow(" KB가 비어있습니다."));
|
|
2655
2454
|
}
|
|
2656
2455
|
console.log();
|
|
@@ -2666,8 +2465,8 @@ kbCmd
|
|
|
2666
2465
|
kbCmd
|
|
2667
2466
|
.command("search <query>")
|
|
2668
2467
|
.description("KB 검색 (시맨틱 + 텍스트 하이브리드)")
|
|
2669
|
-
.option("--bot <name>", "봇 KB도 검색", detectBotId())
|
|
2670
2468
|
.option("--domain <name>", "도메인 필터")
|
|
2469
|
+
.option("--service <name>", "서비스(프로젝트) 필터")
|
|
2671
2470
|
.option("--limit <n>", "최대 결과 수", "10")
|
|
2672
2471
|
.option("--mode <type>", "검색 모드 (hybrid|semantic|text)", "hybrid")
|
|
2673
2472
|
.action(async (query, options) => {
|
|
@@ -2676,7 +2475,7 @@ kbCmd
|
|
|
2676
2475
|
const pool = (0, database_1.getPool)();
|
|
2677
2476
|
const results = await (0, kb_1.kbSearch)(pool, query, {
|
|
2678
2477
|
domain: options.domain,
|
|
2679
|
-
|
|
2478
|
+
service: options.service,
|
|
2680
2479
|
limit: parseInt(options.limit),
|
|
2681
2480
|
mode: options.mode,
|
|
2682
2481
|
});
|
|
@@ -2703,46 +2502,9 @@ kbCmd
|
|
|
2703
2502
|
process.exit(1);
|
|
2704
2503
|
}
|
|
2705
2504
|
});
|
|
2706
|
-
kbCmd
|
|
2707
|
-
.command("diff")
|
|
2708
|
-
.description("로컬 .kb/ vs DB 차이 비교")
|
|
2709
|
-
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2710
|
-
.action(async (options) => {
|
|
2711
|
-
const spinner = (0, ora_1.default)("차이 비교 중...").start();
|
|
2712
|
-
try {
|
|
2713
|
-
const pool = (0, database_1.getPool)();
|
|
2714
|
-
const diff = await (0, kb_1.kbDiff)(pool, options.bot, process.cwd());
|
|
2715
|
-
spinner.stop();
|
|
2716
|
-
console.log(chalk_1.default.cyan.bold("\n📊 KB Diff\n"));
|
|
2717
|
-
console.log(chalk_1.default.green(` ✚ 추가됨 (DB에만 있음): ${diff.added.length}건`));
|
|
2718
|
-
console.log(chalk_1.default.red(` ✖ 삭제됨 (로컬에만 있음): ${diff.removed.length}건`));
|
|
2719
|
-
console.log(chalk_1.default.yellow(` ✎ 변경됨: ${diff.modified.length}건`));
|
|
2720
|
-
console.log(chalk_1.default.gray(` ═ 동일: ${diff.unchanged}건`));
|
|
2721
|
-
if (diff.added.length > 0) {
|
|
2722
|
-
console.log(chalk_1.default.green("\n ✚ 추가된 항목:"));
|
|
2723
|
-
diff.added.forEach(e => console.log(chalk_1.default.gray(` [${e.domain}] ${e.key}`)));
|
|
2724
|
-
}
|
|
2725
|
-
if (diff.removed.length > 0) {
|
|
2726
|
-
console.log(chalk_1.default.red("\n ✖ 삭제된 항목:"));
|
|
2727
|
-
diff.removed.forEach(e => console.log(chalk_1.default.gray(` [${e.domain}] ${e.key}`)));
|
|
2728
|
-
}
|
|
2729
|
-
if (diff.modified.length > 0) {
|
|
2730
|
-
console.log(chalk_1.default.yellow("\n ✎ 변경된 항목:"));
|
|
2731
|
-
diff.modified.forEach(m => console.log(chalk_1.default.gray(` [${m.local.domain}] ${m.local.key}`)));
|
|
2732
|
-
}
|
|
2733
|
-
console.log();
|
|
2734
|
-
await (0, database_1.closeConnection)();
|
|
2735
|
-
}
|
|
2736
|
-
catch (err) {
|
|
2737
|
-
spinner.fail(`Diff 실패: ${err}`);
|
|
2738
|
-
await (0, database_1.closeConnection)();
|
|
2739
|
-
process.exit(1);
|
|
2740
|
-
}
|
|
2741
|
-
});
|
|
2742
2505
|
kbCmd
|
|
2743
2506
|
.command("embed")
|
|
2744
2507
|
.description("기존 KB 항목에 임베딩 벡터 생성 (OPENAI_API_KEY 필요)")
|
|
2745
|
-
.option("--bot <name>", "봇 KB도 임베딩", detectBotId())
|
|
2746
2508
|
.option("--domain <name>", "도메인 필터")
|
|
2747
2509
|
.option("--force", "이미 임베딩된 항목도 재생성")
|
|
2748
2510
|
.action(async (options) => {
|
|
@@ -2755,29 +2517,17 @@ kbCmd
|
|
|
2755
2517
|
try {
|
|
2756
2518
|
const pool = (0, database_1.getPool)();
|
|
2757
2519
|
const client = await pool.connect();
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
const sharedParams = [];
|
|
2520
|
+
let sql = "SELECT kb_id, domain, key, content FROM semo.knowledge_base WHERE 1=1";
|
|
2521
|
+
const params = [];
|
|
2761
2522
|
let pIdx = 1;
|
|
2762
2523
|
if (!options.force)
|
|
2763
|
-
|
|
2524
|
+
sql += " AND embedding IS NULL";
|
|
2764
2525
|
if (options.domain) {
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
}
|
|
2768
|
-
const sharedRows = await client.query(sharedSql, sharedParams);
|
|
2769
|
-
// Bot KB
|
|
2770
|
-
let botSql = "SELECT id, domain, key, content FROM semo.bot_knowledge WHERE bot_id = $1";
|
|
2771
|
-
const botParams = [options.bot];
|
|
2772
|
-
let bIdx = 2;
|
|
2773
|
-
if (!options.force)
|
|
2774
|
-
botSql += " AND embedding IS NULL";
|
|
2775
|
-
if (options.domain) {
|
|
2776
|
-
botSql += ` AND domain = $${bIdx++}`;
|
|
2777
|
-
botParams.push(options.domain);
|
|
2526
|
+
sql += ` AND domain = $${pIdx++}`;
|
|
2527
|
+
params.push(options.domain);
|
|
2778
2528
|
}
|
|
2779
|
-
const
|
|
2780
|
-
const total =
|
|
2529
|
+
const rows = await client.query(sql, params);
|
|
2530
|
+
const total = rows.rows.length;
|
|
2781
2531
|
spinner.succeed(`${total}건 임베딩 대상`);
|
|
2782
2532
|
if (total === 0) {
|
|
2783
2533
|
console.log(chalk_1.default.green(" 모든 항목이 이미 임베딩되어 있습니다."));
|
|
@@ -2787,8 +2537,7 @@ kbCmd
|
|
|
2787
2537
|
}
|
|
2788
2538
|
let done = 0;
|
|
2789
2539
|
const embedSpinner = (0, ora_1.default)(`임베딩 생성 중... 0/${total}`).start();
|
|
2790
|
-
|
|
2791
|
-
for (const row of sharedRows.rows) {
|
|
2540
|
+
for (const row of rows.rows) {
|
|
2792
2541
|
const embedding = await (0, kb_1.generateEmbedding)(`${row.key}: ${row.content}`);
|
|
2793
2542
|
if (embedding) {
|
|
2794
2543
|
await client.query("UPDATE semo.knowledge_base SET embedding = $1::vector WHERE kb_id = $2", [`[${embedding.join(",")}]`, row.kb_id]);
|
|
@@ -2796,15 +2545,6 @@ kbCmd
|
|
|
2796
2545
|
done++;
|
|
2797
2546
|
embedSpinner.text = `임베딩 생성 중... ${done}/${total}`;
|
|
2798
2547
|
}
|
|
2799
|
-
// Process bot KB
|
|
2800
|
-
for (const row of botRows.rows) {
|
|
2801
|
-
const embedding = await (0, kb_1.generateEmbedding)(`${row.key}: ${row.content}`);
|
|
2802
|
-
if (embedding) {
|
|
2803
|
-
await client.query("UPDATE semo.bot_knowledge SET embedding = $1::vector WHERE id = $2", [`[${embedding.join(",")}]`, row.id]);
|
|
2804
|
-
}
|
|
2805
|
-
done++;
|
|
2806
|
-
embedSpinner.text = `임베딩 생성 중... ${done}/${total}`;
|
|
2807
|
-
}
|
|
2808
2548
|
embedSpinner.succeed(`${done}건 임베딩 완료`);
|
|
2809
2549
|
client.release();
|
|
2810
2550
|
await (0, database_1.closeConnection)();
|
|
@@ -2818,7 +2558,6 @@ kbCmd
|
|
|
2818
2558
|
kbCmd
|
|
2819
2559
|
.command("sync")
|
|
2820
2560
|
.description("양방향 동기화 (pull → merge → push)")
|
|
2821
|
-
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2822
2561
|
.option("--domain <name>", "도메인 필터")
|
|
2823
2562
|
.action(async (options) => {
|
|
2824
2563
|
console.log(chalk_1.default.cyan.bold("\n🔄 KB 동기화\n"));
|
|
@@ -2826,8 +2565,8 @@ kbCmd
|
|
|
2826
2565
|
try {
|
|
2827
2566
|
const pool = (0, database_1.getPool)();
|
|
2828
2567
|
// Step 1: Pull
|
|
2829
|
-
const pulled = await (0, kb_1.kbPull)(pool, options.
|
|
2830
|
-
spinner.succeed(`Pull 완료:
|
|
2568
|
+
const pulled = await (0, kb_1.kbPull)(pool, options.domain, process.cwd());
|
|
2569
|
+
spinner.succeed(`Pull 완료: ${pulled.length}건`);
|
|
2831
2570
|
// Step 2: Pull ontology
|
|
2832
2571
|
const spinner2 = (0, ora_1.default)("Step 2/2: 온톨로지 동기화...").start();
|
|
2833
2572
|
const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
|
|
@@ -2850,11 +2589,15 @@ const ontoCmd = program
|
|
|
2850
2589
|
ontoCmd
|
|
2851
2590
|
.command("list")
|
|
2852
2591
|
.description("정의된 온톨로지 도메인 목록")
|
|
2592
|
+
.option("--service <name>", "서비스별 필터")
|
|
2853
2593
|
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
2854
2594
|
.action(async (options) => {
|
|
2855
2595
|
try {
|
|
2856
2596
|
const pool = (0, database_1.getPool)();
|
|
2857
|
-
|
|
2597
|
+
let domains = await (0, kb_1.ontoList)(pool);
|
|
2598
|
+
if (options.service) {
|
|
2599
|
+
domains = domains.filter(d => d.service === options.service || d.domain === options.service || d.domain.startsWith(`${options.service}.`));
|
|
2600
|
+
}
|
|
2858
2601
|
if (options.format === "json") {
|
|
2859
2602
|
console.log(JSON.stringify(domains, null, 2));
|
|
2860
2603
|
}
|
|
@@ -2864,10 +2607,66 @@ ontoCmd
|
|
|
2864
2607
|
console.log(chalk_1.default.yellow(" 온톨로지가 정의되지 않았습니다."));
|
|
2865
2608
|
}
|
|
2866
2609
|
else {
|
|
2610
|
+
// Group by service
|
|
2611
|
+
const global = domains.filter(d => !d.service);
|
|
2612
|
+
const byService = {};
|
|
2867
2613
|
for (const d of domains) {
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2614
|
+
if (d.service) {
|
|
2615
|
+
if (!byService[d.service])
|
|
2616
|
+
byService[d.service] = [];
|
|
2617
|
+
byService[d.service].push(d);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
if (global.length > 0) {
|
|
2621
|
+
console.log(chalk_1.default.white.bold(" Global"));
|
|
2622
|
+
for (const d of global) {
|
|
2623
|
+
const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
|
|
2624
|
+
console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + chalk_1.default.gray(` (v${d.version})`));
|
|
2625
|
+
if (d.description)
|
|
2626
|
+
console.log(chalk_1.default.gray(` ${d.description}`));
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
for (const [svc, svcDomains] of Object.entries(byService)) {
|
|
2630
|
+
console.log(chalk_1.default.white.bold(`\n Service: ${svc}`));
|
|
2631
|
+
for (const d of svcDomains) {
|
|
2632
|
+
const typeStr = d.entity_type ? chalk_1.default.gray(` [${d.entity_type}]`) : "";
|
|
2633
|
+
console.log(chalk_1.default.cyan(` ${d.domain}`) + typeStr + chalk_1.default.gray(` (v${d.version})`));
|
|
2634
|
+
if (d.description)
|
|
2635
|
+
console.log(chalk_1.default.gray(` ${d.description}`));
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
console.log();
|
|
2640
|
+
}
|
|
2641
|
+
await (0, database_1.closeConnection)();
|
|
2642
|
+
}
|
|
2643
|
+
catch (err) {
|
|
2644
|
+
console.error(chalk_1.default.red(`조회 실패: ${err}`));
|
|
2645
|
+
await (0, database_1.closeConnection)();
|
|
2646
|
+
process.exit(1);
|
|
2647
|
+
}
|
|
2648
|
+
});
|
|
2649
|
+
ontoCmd
|
|
2650
|
+
.command("types")
|
|
2651
|
+
.description("온톨로지 타입 목록 (구조적 템플릿)")
|
|
2652
|
+
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
2653
|
+
.action(async (options) => {
|
|
2654
|
+
try {
|
|
2655
|
+
const pool = (0, database_1.getPool)();
|
|
2656
|
+
const types = await (0, kb_1.ontoListTypes)(pool);
|
|
2657
|
+
if (options.format === "json") {
|
|
2658
|
+
console.log(JSON.stringify(types, null, 2));
|
|
2659
|
+
}
|
|
2660
|
+
else {
|
|
2661
|
+
console.log(chalk_1.default.cyan.bold("\n📐 온톨로지 타입\n"));
|
|
2662
|
+
if (types.length === 0) {
|
|
2663
|
+
console.log(chalk_1.default.yellow(" 타입이 정의되지 않았습니다. (016 마이그레이션 실행 필요)"));
|
|
2664
|
+
}
|
|
2665
|
+
else {
|
|
2666
|
+
for (const t of types) {
|
|
2667
|
+
console.log(chalk_1.default.cyan(` ${t.type_key}`) + chalk_1.default.gray(` (v${t.version})`));
|
|
2668
|
+
if (t.description)
|
|
2669
|
+
console.log(chalk_1.default.gray(` ${t.description}`));
|
|
2871
2670
|
}
|
|
2872
2671
|
}
|
|
2873
2672
|
console.log();
|
|
@@ -2957,6 +2756,8 @@ ontoCmd
|
|
|
2957
2756
|
(0, bots_1.registerBotsCommands)(program);
|
|
2958
2757
|
(0, get_1.registerGetCommands)(program);
|
|
2959
2758
|
(0, sessions_1.registerSessionsCommands)(program);
|
|
2759
|
+
(0, db_1.registerDbCommands)(program);
|
|
2760
|
+
(0, memory_1.registerMemoryCommands)(program);
|
|
2960
2761
|
// === semo skills — DB 시딩 ===
|
|
2961
2762
|
/**
|
|
2962
2763
|
* SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
|
|
@@ -2984,119 +2785,12 @@ function parseSkillFrontmatter(content) {
|
|
|
2984
2785
|
// tools 배열
|
|
2985
2786
|
const toolsMatch = fm.match(/^tools:\s*\[(.+)\]$/m);
|
|
2986
2787
|
const tools = toolsMatch ? toolsMatch[1].split(",").map((t) => t.trim()) : [];
|
|
2987
|
-
// category
|
|
2788
|
+
// category
|
|
2988
2789
|
const category = "core";
|
|
2989
2790
|
return { name, description, category, tools };
|
|
2990
2791
|
}
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
*/
|
|
2994
|
-
async function seedSkillsToDb(semoSystemDir) {
|
|
2995
|
-
const skillsDir = path.join(semoSystemDir, "semo-skills");
|
|
2996
|
-
if (!fs.existsSync(skillsDir)) {
|
|
2997
|
-
console.log(chalk_1.default.red(`\n❌ semo-skills 디렉토리를 찾을 수 없습니다: ${skillsDir}`));
|
|
2998
|
-
return;
|
|
2999
|
-
}
|
|
3000
|
-
const connected = await (0, database_1.isDbConnected)();
|
|
3001
|
-
if (!connected) {
|
|
3002
|
-
console.log(chalk_1.default.red("❌ DB 연결 실패 — seed-skills 건너뜀"));
|
|
3003
|
-
return;
|
|
3004
|
-
}
|
|
3005
|
-
const spinner = (0, ora_1.default)("semo-skills 스캔 중...").start();
|
|
3006
|
-
// 활성 스킬 디렉토리 수집 (_archived, CHANGELOG, VERSION 제외)
|
|
3007
|
-
const excludeDirs = new Set(["_archived", "CHANGELOG"]);
|
|
3008
|
-
const excludeFiles = new Set(["VERSION"]);
|
|
3009
|
-
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
3010
|
-
const skillDirs = entries.filter(e => e.isDirectory() && !excludeDirs.has(e.name));
|
|
3011
|
-
const skills = [];
|
|
3012
|
-
for (const dir of skillDirs) {
|
|
3013
|
-
const skillMdPath = path.join(skillsDir, dir.name, "SKILL.md");
|
|
3014
|
-
if (!fs.existsSync(skillMdPath))
|
|
3015
|
-
continue;
|
|
3016
|
-
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
3017
|
-
const parsed = parseSkillFrontmatter(content);
|
|
3018
|
-
if (!parsed)
|
|
3019
|
-
continue;
|
|
3020
|
-
skills.push({ ...parsed, content });
|
|
3021
|
-
}
|
|
3022
|
-
spinner.text = `${skills.length}개 스킬 발견 — DB에 upsert 중...`;
|
|
3023
|
-
const pool = (0, database_1.getPool)();
|
|
3024
|
-
const client = await pool.connect();
|
|
3025
|
-
let upserted = 0;
|
|
3026
|
-
const errors = [];
|
|
3027
|
-
try {
|
|
3028
|
-
await client.query("BEGIN");
|
|
3029
|
-
for (let i = 0; i < skills.length; i++) {
|
|
3030
|
-
const skill = skills[i];
|
|
3031
|
-
try {
|
|
3032
|
-
await client.query(`INSERT INTO semo.skills
|
|
3033
|
-
(name, display_name, description, content, category, package,
|
|
3034
|
-
is_active, is_required, install_order, version)
|
|
3035
|
-
VALUES ($1, $2, $3, $4, $5, $6, true, false, $7, '1.0.0')
|
|
3036
|
-
ON CONFLICT (name) DO UPDATE SET
|
|
3037
|
-
display_name = EXCLUDED.display_name,
|
|
3038
|
-
description = EXCLUDED.description,
|
|
3039
|
-
content = EXCLUDED.content,
|
|
3040
|
-
category = EXCLUDED.category`, [skill.name, skill.name, skill.description, skill.content, skill.category, "semo-skills", i + 1]);
|
|
3041
|
-
upserted++;
|
|
3042
|
-
}
|
|
3043
|
-
catch (err) {
|
|
3044
|
-
errors.push(`${skill.name}: ${err}`);
|
|
3045
|
-
}
|
|
3046
|
-
}
|
|
3047
|
-
await client.query("COMMIT");
|
|
3048
|
-
spinner.succeed(`seed-skills 완료: ${upserted}개 스킬 upsert`);
|
|
3049
|
-
if (errors.length > 0) {
|
|
3050
|
-
errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
catch (err) {
|
|
3054
|
-
await client.query("ROLLBACK");
|
|
3055
|
-
spinner.fail(`seed-skills 실패: ${err}`);
|
|
3056
|
-
}
|
|
3057
|
-
finally {
|
|
3058
|
-
client.release();
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
// `semo skills` 커맨드 그룹
|
|
3062
|
-
const skillsCmd = program
|
|
3063
|
-
.command("skills")
|
|
3064
|
-
.description("스킬 관리 (DB 시딩 등)");
|
|
3065
|
-
skillsCmd
|
|
3066
|
-
.command("seed")
|
|
3067
|
-
.description("semo-system/semo-skills/ → semo.skills DB upsert")
|
|
3068
|
-
.option("--semo-system <path>", "semo-system 경로 (기본: ./semo-system)")
|
|
3069
|
-
.option("--dry-run", "실제 upsert 없이 스캔 결과만 출력")
|
|
3070
|
-
.action(async (options) => {
|
|
3071
|
-
const cwd = process.cwd();
|
|
3072
|
-
const semoSystemDir = options.semoSystem
|
|
3073
|
-
? path.resolve(options.semoSystem)
|
|
3074
|
-
: path.join(cwd, "semo-system");
|
|
3075
|
-
if (options.dryRun) {
|
|
3076
|
-
const skillsDir = path.join(semoSystemDir, "semo-skills");
|
|
3077
|
-
if (!fs.existsSync(skillsDir)) {
|
|
3078
|
-
console.log(chalk_1.default.red(`❌ ${skillsDir} 없음`));
|
|
3079
|
-
process.exit(1);
|
|
3080
|
-
}
|
|
3081
|
-
const excludeDirs = new Set(["_archived", "CHANGELOG"]);
|
|
3082
|
-
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
3083
|
-
const skillDirs = entries.filter(e => e.isDirectory() && !excludeDirs.has(e.name));
|
|
3084
|
-
console.log(chalk_1.default.cyan.bold("\n[dry-run] 발견된 스킬:\n"));
|
|
3085
|
-
for (const dir of skillDirs) {
|
|
3086
|
-
const mdPath = path.join(skillsDir, dir.name, "SKILL.md");
|
|
3087
|
-
if (!fs.existsSync(mdPath))
|
|
3088
|
-
continue;
|
|
3089
|
-
const parsed = parseSkillFrontmatter(fs.readFileSync(mdPath, "utf-8"));
|
|
3090
|
-
if (parsed) {
|
|
3091
|
-
console.log(chalk_1.default.gray(` ${parsed.name.padEnd(20)} ${parsed.description.split("\n")[0].substring(0, 60)}`));
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
console.log();
|
|
3095
|
-
return;
|
|
3096
|
-
}
|
|
3097
|
-
await seedSkillsToDb(semoSystemDir);
|
|
3098
|
-
await (0, database_1.closeConnection)();
|
|
3099
|
-
});
|
|
2792
|
+
// semo skills seed — 제거됨 (중앙 DB 단일 SoT)
|
|
2793
|
+
// 봇 전용 스킬은 semo bots seed로 동기화
|
|
3100
2794
|
// === -v 옵션 처리 (program.parse 전에 직접 처리) ===
|
|
3101
2795
|
async function main() {
|
|
3102
2796
|
const args = process.argv.slice(2);
|