@team-semicolon/semo-cli 4.1.4 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/dist/commands/audit.d.ts +27 -0
- package/dist/commands/audit.js +338 -0
- package/dist/commands/bots.js +524 -24
- package/dist/commands/context.d.ts +14 -3
- package/dist/commands/context.js +192 -113
- package/dist/commands/db.d.ts +9 -0
- package/dist/commands/db.js +189 -0
- package/dist/commands/get.d.ts +1 -2
- package/dist/commands/get.js +24 -116
- package/dist/commands/sessions.d.ts +2 -1
- package/dist/commands/sessions.js +31 -62
- package/dist/commands/skill-sync.d.ts +28 -0
- package/dist/commands/skill-sync.js +111 -0
- package/dist/commands/skill-sync.test.d.ts +16 -0
- package/dist/commands/skill-sync.test.js +186 -0
- package/dist/database.d.ts +41 -3
- package/dist/database.js +128 -554
- package/dist/env-parser.d.ts +5 -0
- package/dist/env-parser.js +27 -0
- package/dist/global-cache.d.ts +12 -0
- package/dist/global-cache.js +184 -0
- package/dist/index.js +352 -817
- package/dist/kb.d.ts +24 -39
- package/dist/kb.js +121 -175
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,11 +56,14 @@ 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 global_cache_1 = require("./global-cache");
|
|
64
67
|
const PACKAGE_NAME = "@team-semicolon/semo-cli";
|
|
65
68
|
// package.json에서 버전 동적 로드
|
|
66
69
|
function getCliVersion() {
|
|
@@ -133,7 +136,7 @@ async function getRemotePackageVersion(packagePath) {
|
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
/**
|
|
136
|
-
* semo-core
|
|
139
|
+
* semo-core 등 원격 버전 가져오기 (semo-system/ 하위 경로)
|
|
137
140
|
*/
|
|
138
141
|
async function getRemoteCoreVersion(type) {
|
|
139
142
|
try {
|
|
@@ -159,7 +162,7 @@ async function showVersionComparison(cwd) {
|
|
|
159
162
|
// 1. CLI 버전 비교
|
|
160
163
|
const currentCliVersion = VERSION;
|
|
161
164
|
const latestCliVersion = await getLatestVersion();
|
|
162
|
-
// 2. semo-core
|
|
165
|
+
// 2. semo-core 버전 비교
|
|
163
166
|
const semoSystemDir = path.join(cwd, "semo-system");
|
|
164
167
|
const hasSemoSystem = fs.existsSync(semoSystemDir);
|
|
165
168
|
const versionInfos = [];
|
|
@@ -171,15 +174,14 @@ async function showVersionComparison(cwd) {
|
|
|
171
174
|
needsUpdate: latestCliVersion ? isVersionLower(currentCliVersion, latestCliVersion) : false,
|
|
172
175
|
level: 0,
|
|
173
176
|
});
|
|
174
|
-
// 레거시 환경 경고 (루트에 semo-core
|
|
177
|
+
// 레거시 환경 경고 (루트에 semo-core가 있는 경우)
|
|
175
178
|
const hasLegacyCore = fs.existsSync(path.join(cwd, "semo-core"));
|
|
176
|
-
|
|
177
|
-
if (hasLegacyCore || hasLegacySkills) {
|
|
179
|
+
if (hasLegacyCore) {
|
|
178
180
|
spinner.warn("레거시 환경 감지됨");
|
|
179
181
|
console.log(chalk_1.default.yellow("\n ⚠️ 구버전 SEMO 구조가 감지되었습니다."));
|
|
180
|
-
console.log(chalk_1.default.gray(" 루트에 semo-core
|
|
182
|
+
console.log(chalk_1.default.gray(" 루트에 semo-core/가 있습니다."));
|
|
181
183
|
console.log(chalk_1.default.cyan("\n 💡 마이그레이션 방법:"));
|
|
182
|
-
console.log(chalk_1.default.gray(" 1. 기존 semo-core
|
|
184
|
+
console.log(chalk_1.default.gray(" 1. 기존 semo-core/ 폴더 삭제"));
|
|
183
185
|
console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
|
|
184
186
|
console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
|
|
185
187
|
console.log(chalk_1.default.gray(" 또는: semo migrate --force\n"));
|
|
@@ -198,19 +200,7 @@ async function showVersionComparison(cwd) {
|
|
|
198
200
|
level: 0,
|
|
199
201
|
});
|
|
200
202
|
}
|
|
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
|
-
}
|
|
203
|
+
// semo-skills 제거됨 — 중앙 DB 단일 SoT
|
|
214
204
|
// semo-agents (semo-system/ 내부)
|
|
215
205
|
const agentsPathSystem = path.join(semoSystemDir, "semo-agents", "VERSION");
|
|
216
206
|
if (fs.existsSync(agentsPathSystem)) {
|
|
@@ -384,13 +374,13 @@ function isSymlinkValid(linkPath) {
|
|
|
384
374
|
}
|
|
385
375
|
/**
|
|
386
376
|
* 레거시 SEMO 환경을 감지합니다.
|
|
387
|
-
* 레거시: 프로젝트 루트에 semo-core
|
|
377
|
+
* 레거시: 프로젝트 루트에 semo-core/ 가 직접 있는 경우
|
|
388
378
|
* 신규: semo-system/ 하위에 있는 경우
|
|
389
379
|
*/
|
|
390
380
|
function detectLegacyEnvironment(cwd) {
|
|
391
381
|
const legacyPaths = [];
|
|
392
382
|
// 루트에 직접 있는 레거시 디렉토리 확인
|
|
393
|
-
const legacyDirs = ["semo-core", "
|
|
383
|
+
const legacyDirs = ["semo-core", "sax-core", "sax-skills"];
|
|
394
384
|
for (const dir of legacyDirs) {
|
|
395
385
|
const dirPath = path.join(cwd, dir);
|
|
396
386
|
if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
|
|
@@ -452,7 +442,7 @@ async function migrateLegacyEnvironment(cwd) {
|
|
|
452
442
|
if (!shouldMigrate) {
|
|
453
443
|
console.log(chalk_1.default.yellow("\n마이그레이션이 취소되었습니다."));
|
|
454
444
|
console.log(chalk_1.default.gray("💡 수동 마이그레이션 방법:"));
|
|
455
|
-
console.log(chalk_1.default.gray(" 1. 기존
|
|
445
|
+
console.log(chalk_1.default.gray(" 1. 기존 레거시 폴더 삭제"));
|
|
456
446
|
console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
|
|
457
447
|
console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
|
|
458
448
|
return false;
|
|
@@ -460,7 +450,7 @@ async function migrateLegacyEnvironment(cwd) {
|
|
|
460
450
|
const spinner = (0, ora_1.default)("레거시 환경 마이그레이션 중...").start();
|
|
461
451
|
try {
|
|
462
452
|
// 1. 루트의 레거시 디렉토리 삭제
|
|
463
|
-
const legacyDirs = ["semo-core", "
|
|
453
|
+
const legacyDirs = ["semo-core", "sax-core", "sax-skills"];
|
|
464
454
|
for (const dir of legacyDirs) {
|
|
465
455
|
const dirPath = path.join(cwd, dir);
|
|
466
456
|
if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
|
|
@@ -579,21 +569,6 @@ async function showVersionInfo() {
|
|
|
579
569
|
level: 0,
|
|
580
570
|
});
|
|
581
571
|
}
|
|
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
572
|
// 결과 출력
|
|
598
573
|
const needsUpdateCount = versionInfos.filter(v => v.needsUpdate).length;
|
|
599
574
|
if (versionInfos.length === 1) {
|
|
@@ -676,11 +651,6 @@ async function confirmOverwrite(itemName, itemPath) {
|
|
|
676
651
|
if (!fs.existsSync(itemPath)) {
|
|
677
652
|
return true;
|
|
678
653
|
}
|
|
679
|
-
// 비인터랙티브 환경(CI, 파이프) — 덮어쓰지 않고 기존 파일 유지
|
|
680
|
-
if (!process.stdin.isTTY) {
|
|
681
|
-
console.log(chalk_1.default.gray(` → ${itemName} 이미 존재 (비인터랙티브 모드: 건너뜀)`));
|
|
682
|
-
return false;
|
|
683
|
-
}
|
|
684
654
|
const { shouldOverwrite } = await inquirer_1.default.prompt([
|
|
685
655
|
{
|
|
686
656
|
type: "confirm",
|
|
@@ -769,36 +739,75 @@ async function showToolsStatus() {
|
|
|
769
739
|
}
|
|
770
740
|
return true;
|
|
771
741
|
}
|
|
772
|
-
// ===
|
|
742
|
+
// === 글로벌 설정 체크 ===
|
|
743
|
+
function isGlobalSetupDone() {
|
|
744
|
+
const home = os.homedir();
|
|
745
|
+
return (fs.existsSync(path.join(home, ".semo.env")) &&
|
|
746
|
+
fs.existsSync(path.join(home, ".claude", "skills")));
|
|
747
|
+
}
|
|
748
|
+
// === onboarding 명령어 (글로벌 1회 설정) ===
|
|
773
749
|
program
|
|
774
|
-
.command("
|
|
775
|
-
.description("
|
|
750
|
+
.command("onboarding")
|
|
751
|
+
.description("글로벌 SEMO 설정 (머신당 1회) — ~/.claude/, ~/.semo.env")
|
|
752
|
+
.option("--credentials-gist <gistId>", "Private GitHub Gist에서 DB 접속정보 가져오기")
|
|
776
753
|
.option("-f, --force", "기존 설정 덮어쓰기")
|
|
777
754
|
.option("--skip-mcp", "MCP 설정 생략")
|
|
755
|
+
.action(async (options) => {
|
|
756
|
+
console.log(chalk_1.default.cyan.bold("\n🏠 SEMO 글로벌 온보딩\n"));
|
|
757
|
+
console.log(chalk_1.default.gray(" 대상: ~/.claude/, ~/.semo.env (머신당 1회)\n"));
|
|
758
|
+
// 1. ~/.semo.env DB 접속 설정
|
|
759
|
+
await setupSemoEnv(options.credentialsGist, options.force);
|
|
760
|
+
// 2. DB health check
|
|
761
|
+
const spinner = (0, ora_1.default)("DB 연결 확인 중...").start();
|
|
762
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
763
|
+
if (connected) {
|
|
764
|
+
spinner.succeed("DB 연결 확인됨");
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
spinner.warn("DB 연결 실패 — 스킬/커맨드/에이전트 설치를 건너뜁니다");
|
|
768
|
+
console.log(chalk_1.default.gray(" ~/.semo.env를 확인하고 다시 시도하세요: semo onboarding\n"));
|
|
769
|
+
await (0, database_1.closeConnection)();
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
// 3. Standard 설치 (DB → ~/.claude/skills, commands, agents)
|
|
773
|
+
await setupStandardGlobal();
|
|
774
|
+
// 4. Hooks 설치 (프로젝트 무관)
|
|
775
|
+
await setupHooks(false);
|
|
776
|
+
// 5. MCP 설정 (글로벌)
|
|
777
|
+
if (!options.skipMcp) {
|
|
778
|
+
await setupMCP(os.homedir(), [], options.force || false);
|
|
779
|
+
}
|
|
780
|
+
await (0, database_1.closeConnection)();
|
|
781
|
+
// 결과 요약
|
|
782
|
+
console.log(chalk_1.default.green.bold("\n✅ SEMO 글로벌 온보딩 완료!\n"));
|
|
783
|
+
console.log(chalk_1.default.cyan("설치된 구성:"));
|
|
784
|
+
console.log(chalk_1.default.gray(" ~/.semo.env DB 접속정보 (권한 600)"));
|
|
785
|
+
console.log(chalk_1.default.gray(" ~/.claude/skills/ 팀 스킬 (DB 기반)"));
|
|
786
|
+
console.log(chalk_1.default.gray(" ~/.claude/commands/ 팀 커맨드 (DB 기반)"));
|
|
787
|
+
console.log(chalk_1.default.gray(" ~/.claude/agents/ 팀 에이전트 (DB 기반, dedup)"));
|
|
788
|
+
console.log(chalk_1.default.gray(" ~/.claude/settings.local.json SessionStart/Stop 훅"));
|
|
789
|
+
console.log(chalk_1.default.cyan("\n다음 단계:"));
|
|
790
|
+
console.log(chalk_1.default.gray(" 프로젝트 디렉토리에서 'semo init'을 실행하세요."));
|
|
791
|
+
console.log();
|
|
792
|
+
});
|
|
793
|
+
// === init 명령어 (프로젝트별 설정) ===
|
|
794
|
+
program
|
|
795
|
+
.command("init")
|
|
796
|
+
.description("현재 프로젝트에 SEMO 프로젝트 설정을 합니다 (글로벌: semo onboarding)")
|
|
797
|
+
.option("-f, --force", "기존 설정 덮어쓰기")
|
|
778
798
|
.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
799
|
.action(async (options) => {
|
|
783
|
-
console.log(chalk_1.default.cyan.bold("\n
|
|
800
|
+
console.log(chalk_1.default.cyan.bold("\n📁 SEMO 프로젝트 설정\n"));
|
|
784
801
|
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);
|
|
802
|
+
// 0. 글로벌 설정 확인
|
|
803
|
+
if (!isGlobalSetupDone()) {
|
|
804
|
+
console.log(chalk_1.default.yellow("⚠ 글로벌 설정이 완료되지 않았습니다."));
|
|
805
|
+
console.log(chalk_1.default.gray(" 먼저 'semo onboarding'을 실행하세요.\n"));
|
|
806
|
+
console.log(chalk_1.default.gray(" 이 머신에서 처음 SEMO를 사용하시나요?"));
|
|
807
|
+
console.log(chalk_1.default.cyan(" → semo onboarding --credentials-gist <GIST_ID>\n"));
|
|
808
|
+
process.exit(1);
|
|
800
809
|
}
|
|
801
|
-
// 1.
|
|
810
|
+
// 1. Git 레포지토리 확인
|
|
802
811
|
const spinner = (0, ora_1.default)("Git 레포지토리 확인 중...").start();
|
|
803
812
|
try {
|
|
804
813
|
(0, child_process_1.execSync)("git rev-parse --git-dir", { cwd, stdio: "pipe" });
|
|
@@ -814,64 +823,48 @@ program
|
|
|
814
823
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
815
824
|
console.log(chalk_1.default.green("\n✓ .claude/ 디렉토리 생성됨"));
|
|
816
825
|
}
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
826
|
+
// 3. 로컬 스킬 경고 (기존 프로젝트 호환)
|
|
827
|
+
const localSkillsDir = path.join(claudeDir, "skills");
|
|
828
|
+
if (fs.existsSync(localSkillsDir)) {
|
|
829
|
+
try {
|
|
830
|
+
const localSkills = fs.readdirSync(localSkillsDir).filter(f => fs.statSync(path.join(localSkillsDir, f)).isDirectory());
|
|
831
|
+
if (localSkills.length > 0) {
|
|
832
|
+
console.log(chalk_1.default.yellow(`\nℹ 프로젝트 로컬 스킬이 감지되었습니다 (${localSkills.length}개).`));
|
|
833
|
+
console.log(chalk_1.default.gray(" 글로벌 스킬(~/.claude/skills/)이 우선 적용됩니다."));
|
|
834
|
+
console.log(chalk_1.default.gray(" 로컬 스킬을 제거하려면: rm -rf .claude/skills/ .claude/commands/ .claude/agents/\n"));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
catch {
|
|
838
|
+
// ignore
|
|
839
|
+
}
|
|
824
840
|
}
|
|
825
|
-
//
|
|
841
|
+
// 4. Context Mesh 초기화
|
|
826
842
|
await setupContextMesh(cwd);
|
|
843
|
+
// 5. CLAUDE.md 생성 (프로젝트 규칙만, 스킬 목록 없음)
|
|
844
|
+
await setupClaudeMd(cwd, [], options.force || false);
|
|
827
845
|
// 6. .gitignore 업데이트
|
|
828
846
|
if (options.gitignore !== false) {
|
|
829
847
|
updateGitignore(cwd);
|
|
830
848
|
}
|
|
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
849
|
// 완료 메시지
|
|
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 (훅 등록)"));
|
|
850
|
+
console.log(chalk_1.default.green.bold("\n✅ SEMO 프로젝트 설정 완료!\n"));
|
|
851
|
+
console.log(chalk_1.default.cyan("생성된 파일:"));
|
|
852
|
+
console.log(chalk_1.default.gray(" {cwd}/.claude/CLAUDE.md 프로젝트 규칙"));
|
|
853
|
+
console.log(chalk_1.default.gray(" {cwd}/.claude/memory/context.md 프로젝트 상태"));
|
|
854
|
+
console.log(chalk_1.default.gray(" {cwd}/.claude/memory/decisions.md ADR"));
|
|
855
|
+
console.log(chalk_1.default.gray(" {cwd}/.claude/memory/projects.md 프로젝트 맵"));
|
|
857
856
|
console.log(chalk_1.default.cyan("\n다음 단계:"));
|
|
858
857
|
console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기 (SessionStart 훅이 자동 sync)"));
|
|
859
858
|
console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
|
|
860
859
|
console.log(chalk_1.default.gray(" 3. /SEMO:help로 도움말 확인"));
|
|
861
860
|
console.log();
|
|
862
|
-
console.log(chalk_1.default.gray(" DB 접속정보 변경: nano ~/.semo.env"));
|
|
863
|
-
console.log();
|
|
864
861
|
});
|
|
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"));
|
|
862
|
+
// === Standard 설치 (DB 기반, 글로벌 ~/.claude/) ===
|
|
863
|
+
async function setupStandardGlobal() {
|
|
864
|
+
console.log(chalk_1.default.cyan("\n📚 Standard 설치 (DB → ~/.claude/)"));
|
|
865
|
+
console.log(chalk_1.default.gray(" 스킬/커맨드/에이전트를 글로벌에 설치\n"));
|
|
872
866
|
const spinner = (0, ora_1.default)("DB에서 스킬/커맨드/에이전트 조회 중...").start();
|
|
873
867
|
try {
|
|
874
|
-
// DB 연결 확인
|
|
875
868
|
const connected = await (0, database_1.isDbConnected)();
|
|
876
869
|
if (connected) {
|
|
877
870
|
spinner.text = "DB 연결 성공, 데이터 조회 중...";
|
|
@@ -879,174 +872,18 @@ async function setupStandard(cwd, force) {
|
|
|
879
872
|
else {
|
|
880
873
|
spinner.text = "DB 연결 실패, 폴백 데이터 사용 중...";
|
|
881
874
|
}
|
|
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 기반)");
|
|
875
|
+
const result = await (0, global_cache_1.syncGlobalCache)();
|
|
876
|
+
console.log(chalk_1.default.green(` ✓ skills 설치 완료 (${result.skills}개)`));
|
|
877
|
+
console.log(chalk_1.default.green(` ✓ commands 설치 완료 (${result.commands}개)`));
|
|
878
|
+
console.log(chalk_1.default.green(` ✓ agents 설치 완료 (${result.agents}개)`));
|
|
879
|
+
spinner.succeed("Standard 설치 완료 (DB → ~/.claude/)");
|
|
936
880
|
}
|
|
937
881
|
catch (error) {
|
|
938
882
|
spinner.fail("Standard 설치 실패");
|
|
939
883
|
console.error(chalk_1.default.red(` ${error}`));
|
|
940
884
|
}
|
|
941
885
|
}
|
|
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
|
-
}
|
|
886
|
+
// (generateClaudeMd removed — setupClaudeMd handles project CLAUDE.md generation)
|
|
1050
887
|
// === Standard 심볼릭 링크 (레거시 호환) ===
|
|
1051
888
|
async function createStandardSymlinks(cwd) {
|
|
1052
889
|
const claudeDir = path.join(cwd, ".claude");
|
|
@@ -1070,29 +907,9 @@ async function createStandardSymlinks(cwd) {
|
|
|
1070
907
|
}
|
|
1071
908
|
console.log(chalk_1.default.green(` ✓ .claude/agents/ (${agents.length}개 agent 링크됨)`));
|
|
1072
909
|
}
|
|
1073
|
-
// skills
|
|
910
|
+
// skills — 중앙 DB 단일 SoT (semo-skills 파일시스템 제거됨)
|
|
1074
911
|
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
|
-
}
|
|
912
|
+
fs.mkdirSync(claudeSkillsDir, { recursive: true });
|
|
1096
913
|
// commands 링크
|
|
1097
914
|
const commandsDir = path.join(claudeDir, "commands");
|
|
1098
915
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
@@ -1183,15 +1000,10 @@ function verifyInstallation(cwd, installedExtensions = []) {
|
|
|
1183
1000
|
}
|
|
1184
1001
|
// === 레거시: semo-system 기반 설치 검증 ===
|
|
1185
1002
|
const coreDir = path.join(semoSystemDir, "semo-core");
|
|
1186
|
-
const skillsDir = path.join(semoSystemDir, "semo-skills");
|
|
1187
1003
|
if (!fs.existsSync(coreDir)) {
|
|
1188
1004
|
result.errors.push("semo-core가 설치되지 않았습니다");
|
|
1189
1005
|
result.success = false;
|
|
1190
1006
|
}
|
|
1191
|
-
if (!fs.existsSync(skillsDir)) {
|
|
1192
|
-
result.errors.push("semo-skills가 설치되지 않았습니다");
|
|
1193
|
-
result.success = false;
|
|
1194
|
-
}
|
|
1195
1007
|
// 2. agents 링크 검증 (isSymlinkValid 사용)
|
|
1196
1008
|
const claudeAgentsDir = path.join(claudeDir, "agents");
|
|
1197
1009
|
const coreAgentsDir = path.join(coreDir, "agents");
|
|
@@ -1218,31 +1030,7 @@ function verifyInstallation(cwd, installedExtensions = []) {
|
|
|
1218
1030
|
}
|
|
1219
1031
|
}
|
|
1220
1032
|
}
|
|
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
|
-
}
|
|
1033
|
+
// 3. skills — 중앙 DB 단일 SoT (파일시스템 검증 불필요)
|
|
1246
1034
|
// 4. commands 검증 (isSymlinkValid 사용)
|
|
1247
1035
|
const semoCommandsLink = path.join(claudeDir, "commands", "SEMO");
|
|
1248
1036
|
try {
|
|
@@ -1372,110 +1160,156 @@ const BASE_MCP_SERVERS = [
|
|
|
1372
1160
|
name: "context7",
|
|
1373
1161
|
command: "npx",
|
|
1374
1162
|
args: ["-y", "@upstash/context7-mcp"],
|
|
1163
|
+
scope: "user",
|
|
1375
1164
|
},
|
|
1376
1165
|
{
|
|
1377
1166
|
name: "sequential-thinking",
|
|
1378
1167
|
command: "npx",
|
|
1379
1168
|
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
|
1169
|
+
scope: "user",
|
|
1380
1170
|
},
|
|
1381
1171
|
{
|
|
1382
1172
|
name: "playwright",
|
|
1383
1173
|
command: "npx",
|
|
1384
1174
|
args: ["-y", "@anthropic-ai/mcp-server-playwright"],
|
|
1175
|
+
scope: "user",
|
|
1385
1176
|
},
|
|
1386
1177
|
{
|
|
1387
1178
|
name: "github",
|
|
1388
1179
|
command: "npx",
|
|
1389
1180
|
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
1181
|
+
scope: "user",
|
|
1182
|
+
},
|
|
1183
|
+
];
|
|
1184
|
+
const SEMO_CREDENTIALS = [
|
|
1185
|
+
{
|
|
1186
|
+
key: "DATABASE_URL",
|
|
1187
|
+
required: true,
|
|
1188
|
+
sensitive: true,
|
|
1189
|
+
description: "팀 코어 PostgreSQL 연결 URL",
|
|
1190
|
+
promptMessage: "DATABASE_URL:",
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
key: "OPENAI_API_KEY",
|
|
1194
|
+
required: false,
|
|
1195
|
+
sensitive: true,
|
|
1196
|
+
description: "OpenAI API 키 (KB 임베딩용)",
|
|
1197
|
+
promptMessage: "OPENAI_API_KEY (없으면 Enter):",
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
key: "SLACK_WEBHOOK",
|
|
1201
|
+
required: false,
|
|
1202
|
+
sensitive: false,
|
|
1203
|
+
description: "Slack 알림 Webhook (선택)",
|
|
1204
|
+
promptMessage: "SLACK_WEBHOOK (없으면 Enter):",
|
|
1390
1205
|
},
|
|
1391
1206
|
];
|
|
1392
|
-
|
|
1393
|
-
function writeSemoEnvFile(dbUrl, slackWebhook = "") {
|
|
1207
|
+
function writeSemoEnvFile(creds) {
|
|
1394
1208
|
const envFile = path.join(os.homedir(), ".semo.env");
|
|
1395
|
-
const
|
|
1396
|
-
#
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1209
|
+
const lines = [
|
|
1210
|
+
"# SEMO 환경변수 — 모든 컨텍스트에서 자동 로드됨",
|
|
1211
|
+
"# (Claude Code 앱, OpenClaw LaunchAgent, cron 등)",
|
|
1212
|
+
"",
|
|
1213
|
+
];
|
|
1214
|
+
// 레지스트리 키 먼저 (순서 보장)
|
|
1215
|
+
for (const def of SEMO_CREDENTIALS) {
|
|
1216
|
+
const val = creds[def.key] || "";
|
|
1217
|
+
lines.push(`# ${def.description}`);
|
|
1218
|
+
lines.push(`${def.key}='${val}'`);
|
|
1219
|
+
lines.push("");
|
|
1220
|
+
}
|
|
1221
|
+
// 레지스트리 외 추가 키
|
|
1222
|
+
for (const [k, v] of Object.entries(creds)) {
|
|
1223
|
+
if (!SEMO_CREDENTIALS.some((d) => d.key === k)) {
|
|
1224
|
+
lines.push(`${k}='${v}'`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
lines.push("");
|
|
1228
|
+
fs.writeFileSync(envFile, lines.join("\n"), { mode: 0o600 });
|
|
1404
1229
|
}
|
|
1405
|
-
function
|
|
1230
|
+
function readSemoEnvCreds() {
|
|
1406
1231
|
const envFile = path.join(os.homedir(), ".semo.env");
|
|
1407
1232
|
if (!fs.existsSync(envFile))
|
|
1408
|
-
return
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1233
|
+
return {};
|
|
1234
|
+
try {
|
|
1235
|
+
return (0, env_parser_1.parseEnvContent)(fs.readFileSync(envFile, "utf-8"));
|
|
1236
|
+
}
|
|
1237
|
+
catch {
|
|
1238
|
+
return {};
|
|
1239
|
+
}
|
|
1412
1240
|
}
|
|
1413
|
-
function
|
|
1241
|
+
function fetchCredsFromGist(gistId) {
|
|
1414
1242
|
try {
|
|
1415
1243
|
const raw = (0, child_process_1.execSync)(`gh gist view ${gistId} --raw`, {
|
|
1416
1244
|
encoding: "utf-8",
|
|
1417
1245
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1418
1246
|
timeout: 8000,
|
|
1419
1247
|
});
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
return match ? match[1].trim() : null;
|
|
1248
|
+
const creds = (0, env_parser_1.parseEnvContent)(raw);
|
|
1249
|
+
return Object.keys(creds).length > 0 ? creds : null;
|
|
1423
1250
|
}
|
|
1424
1251
|
catch {
|
|
1425
1252
|
return null;
|
|
1426
1253
|
}
|
|
1427
1254
|
}
|
|
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
|
|
1255
|
+
async function setupSemoEnv(credentialsGist, force) {
|
|
1256
|
+
console.log(chalk_1.default.cyan("\n🔑 환경변수 설정"));
|
|
1257
|
+
// 1. 기존 파일
|
|
1258
|
+
const existing = force ? {} : readSemoEnvCreds();
|
|
1259
|
+
// 2. Gist
|
|
1447
1260
|
const gistId = credentialsGist || process.env.SEMO_CREDENTIALS_GIST;
|
|
1261
|
+
let gistCreds = {};
|
|
1448
1262
|
if (gistId) {
|
|
1449
1263
|
console.log(chalk_1.default.gray(" GitHub Gist에서 팀 접속정보 가져오는 중..."));
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1264
|
+
gistCreds = fetchCredsFromGist(gistId) || {};
|
|
1265
|
+
}
|
|
1266
|
+
// 3. 머지: gist(base) ← file(override) ← env(override)
|
|
1267
|
+
const merged = { ...gistCreds };
|
|
1268
|
+
for (const [k, v] of Object.entries(existing)) {
|
|
1269
|
+
if (v)
|
|
1270
|
+
merged[k] = v;
|
|
1271
|
+
}
|
|
1272
|
+
for (const def of SEMO_CREDENTIALS) {
|
|
1273
|
+
if (process.env[def.key])
|
|
1274
|
+
merged[def.key] = process.env[def.key];
|
|
1275
|
+
}
|
|
1276
|
+
// 4. required인데 없는 키만 프롬프트
|
|
1277
|
+
let hasNewKeys = false;
|
|
1278
|
+
for (const def of SEMO_CREDENTIALS) {
|
|
1279
|
+
if (merged[def.key]) {
|
|
1280
|
+
const label = def.sensitive ? "(설정됨)" : merged[def.key];
|
|
1281
|
+
console.log(chalk_1.default.green(` ✅ ${def.key} ${label}`));
|
|
1282
|
+
}
|
|
1283
|
+
else if (def.required) {
|
|
1284
|
+
const { value } = await inquirer_1.default.prompt([
|
|
1285
|
+
{
|
|
1286
|
+
type: "password",
|
|
1287
|
+
name: "value",
|
|
1288
|
+
message: def.promptMessage,
|
|
1289
|
+
mask: "*",
|
|
1290
|
+
},
|
|
1291
|
+
]);
|
|
1292
|
+
if (value?.trim()) {
|
|
1293
|
+
merged[def.key] = value.trim();
|
|
1294
|
+
hasNewKeys = true;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
console.log(chalk_1.default.gray(` ⏭ ${def.key} (없음 — 선택사항)`));
|
|
1455
1299
|
}
|
|
1456
|
-
console.log(chalk_1.default.yellow(" ⚠️ Gist fetch 실패 — 수동 입력으로 전환"));
|
|
1457
1300
|
}
|
|
1458
|
-
//
|
|
1459
|
-
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
},
|
|
1467
|
-
]);
|
|
1468
|
-
if (dbUrl && dbUrl.trim()) {
|
|
1469
|
-
writeSemoEnvFile(dbUrl.trim());
|
|
1301
|
+
// 5. 변경사항이 있거나 파일이 없으면 쓰기
|
|
1302
|
+
const envFile = path.join(os.homedir(), ".semo.env");
|
|
1303
|
+
const needsWrite = force ||
|
|
1304
|
+
hasNewKeys ||
|
|
1305
|
+
!fs.existsSync(envFile) ||
|
|
1306
|
+
Object.keys(gistCreds).some((k) => !existing[k]);
|
|
1307
|
+
if (needsWrite) {
|
|
1308
|
+
writeSemoEnvFile(merged);
|
|
1470
1309
|
console.log(chalk_1.default.green(" ✅ ~/.semo.env 저장됨 (권한: 600)"));
|
|
1471
|
-
console.log(chalk_1.default.gray(` 파일: ${envFile}`));
|
|
1472
1310
|
}
|
|
1473
1311
|
else {
|
|
1474
|
-
|
|
1475
|
-
if (!fs.existsSync(envFile)) {
|
|
1476
|
-
writeSemoEnvFile("");
|
|
1477
|
-
}
|
|
1478
|
-
console.log(chalk_1.default.yellow(" ⚠️ 건너뜀 — ~/.semo.env에서 DATABASE_URL을 직접 입력하세요"));
|
|
1312
|
+
console.log(chalk_1.default.gray(" ~/.semo.env 변경 없음"));
|
|
1479
1313
|
}
|
|
1480
1314
|
}
|
|
1481
1315
|
// === Claude MCP 서버 존재 여부 확인 ===
|
|
@@ -1504,6 +1338,9 @@ function registerMCPServer(server) {
|
|
|
1504
1338
|
args.push("-e", `${key}=${value}`);
|
|
1505
1339
|
}
|
|
1506
1340
|
}
|
|
1341
|
+
// scope 지정 (기본: project)
|
|
1342
|
+
const scope = server.scope || "project";
|
|
1343
|
+
args.push("-s", scope);
|
|
1507
1344
|
// -- 구분자 후 명령어와 인자 추가
|
|
1508
1345
|
args.push("--", server.command, ...server.args);
|
|
1509
1346
|
(0, child_process_1.execSync)(`claude ${args.join(" ")}`, { stdio: "pipe" });
|
|
@@ -1529,23 +1366,17 @@ async function setupMCP(cwd, _extensions, force) {
|
|
|
1529
1366
|
const settings = {
|
|
1530
1367
|
mcpServers: {},
|
|
1531
1368
|
};
|
|
1532
|
-
//
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
args: server.args,
|
|
1539
|
-
};
|
|
1540
|
-
if (server.env) {
|
|
1541
|
-
serverConfig.env = server.env;
|
|
1542
|
-
}
|
|
1543
|
-
settings.mcpServers[server.name] = serverConfig;
|
|
1544
|
-
}
|
|
1369
|
+
// settings.json에는 프로젝트 전용 semo-kb만 기록
|
|
1370
|
+
// 공통 서버(context7 등)는 유저레벨에 등록하므로 프로젝트 settings에 쓰지 않음
|
|
1371
|
+
settings.mcpServers["semo-kb"] = {
|
|
1372
|
+
command: "node",
|
|
1373
|
+
args: ["packages/mcp-kb/dist/index.js"],
|
|
1374
|
+
};
|
|
1545
1375
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
1546
|
-
console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨 (MCP 설정
|
|
1547
|
-
// Claude Code에 MCP 서버 등록 시도
|
|
1376
|
+
console.log(chalk_1.default.green("✓ .claude/settings.json 생성됨 (semo-kb MCP 설정)"));
|
|
1377
|
+
// Claude Code에 MCP 서버 등록 시도 (공통 서버는 유저레벨로)
|
|
1548
1378
|
console.log(chalk_1.default.cyan("\n🔌 Claude Code에 MCP 서버 등록 중..."));
|
|
1379
|
+
const allServers = [...BASE_MCP_SERVERS];
|
|
1549
1380
|
const successServers = [];
|
|
1550
1381
|
const skippedServers = [];
|
|
1551
1382
|
const failedServers = [];
|
|
@@ -1628,12 +1459,13 @@ semo-system/
|
|
|
1628
1459
|
}
|
|
1629
1460
|
}
|
|
1630
1461
|
// === Hooks 설치/업데이트 ===
|
|
1631
|
-
async function setupHooks(
|
|
1462
|
+
async function setupHooks(isUpdate = false) {
|
|
1632
1463
|
const action = isUpdate ? "업데이트" : "설치";
|
|
1633
1464
|
console.log(chalk_1.default.cyan(`\n🪝 Claude Code Hooks ${action}`));
|
|
1634
|
-
|
|
1465
|
+
console.log(chalk_1.default.gray(" semo CLI 기반 컨텍스트 동기화\n"));
|
|
1466
|
+
const homeDir = os.homedir();
|
|
1635
1467
|
const settingsPath = path.join(homeDir, ".claude", "settings.local.json");
|
|
1636
|
-
//
|
|
1468
|
+
// hooks 설정 객체 — semo CLI만 사용 (프로젝트 경로 무관)
|
|
1637
1469
|
const hooksConfig = {
|
|
1638
1470
|
SessionStart: [
|
|
1639
1471
|
{
|
|
@@ -1660,36 +1492,6 @@ async function setupHooks(cwd, isUpdate = false) {
|
|
|
1660
1492
|
},
|
|
1661
1493
|
],
|
|
1662
1494
|
};
|
|
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
1495
|
// 기존 설정 로드 또는 새로 생성
|
|
1694
1496
|
let existingSettings = {};
|
|
1695
1497
|
const claudeConfigDir = path.join(homeDir, ".claude");
|
|
@@ -1890,132 +1692,86 @@ async function setupClaudeMd(cwd, _extensions, force) {
|
|
|
1890
1692
|
return;
|
|
1891
1693
|
}
|
|
1892
1694
|
}
|
|
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와 자동 동기화됩니다.
|
|
1695
|
+
// 프로젝트 규칙만 (스킬/에이전트 목록은 글로벌 ~/.claude/에 있음)
|
|
1696
|
+
const claudeMdContent = `# SEMO Project Configuration
|
|
1923
1697
|
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
| 세션 시작 | \`semo context sync\` → \`.claude/memory/\` 최신화 |
|
|
1927
|
-
| 세션 종료 | \`semo context push\` → \`decisions.md\` 변경분 DB 저장 |
|
|
1698
|
+
> SEMO (Semicolon Orchestrate) - AI Agent Orchestration Framework v${VERSION}
|
|
1699
|
+
> 스킬/에이전트/커맨드는 글로벌(~/.claude/)에서 로드됩니다.
|
|
1928
1700
|
|
|
1929
1701
|
---
|
|
1930
1702
|
|
|
1931
|
-
##
|
|
1932
|
-
|
|
1933
|
-
\`.claude/memory/\` 파일들은 **팀 Core DB (\`semo\` 스키마)에서 자동으로 채워집니다**.
|
|
1934
|
-
직접 편집하지 마세요 — 세션 시작 시 덮어씌워집니다.
|
|
1703
|
+
## KB 접근 (semo-kb MCP 서버)
|
|
1935
1704
|
|
|
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 → 로컬 (읽기 전용) |
|
|
1705
|
+
KB 데이터는 **semo-kb MCP 서버**를 통해 Core DB에서 실시간 조회합니다.
|
|
1945
1706
|
|
|
1946
|
-
|
|
1707
|
+
| MCP 도구 | 설명 |
|
|
1708
|
+
|----------|------|
|
|
1709
|
+
| \`kb_search\` | 벡터+텍스트 하이브리드 검색 (query, domain?, limit?, mode?) |
|
|
1710
|
+
| \`kb_get\` | domain+key 정확 조회 |
|
|
1711
|
+
| \`kb_list\` | 도메인별 엔트리 목록 |
|
|
1712
|
+
| \`kb_upsert\` | KB 항목 쓰기 (OpenAI 임베딩 자동 생성) |
|
|
1713
|
+
| \`kb_bot_status\` | 봇 상태 테이블 조회 |
|
|
1714
|
+
| \`kb_ontology\` | 온톨로지 스키마 조회 |
|
|
1715
|
+
| \`kb_digest\` | 봇 구독 도메인 변경 다이제스트 |
|
|
1947
1716
|
|
|
1948
1717
|
---
|
|
1949
1718
|
|
|
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\`에 있습니다.
|
|
1719
|
+
## KB-First 행동 규칙 (NON-NEGOTIABLE)
|
|
1968
1720
|
|
|
1969
|
-
|
|
1721
|
+
> KB는 팀의 Single Source of Truth이다. 아래 규칙은 예외 없이 적용된다.
|
|
1970
1722
|
|
|
1971
|
-
|
|
1723
|
+
### 읽기 (Query-First)
|
|
1724
|
+
다음 주제 질문 → **반드시 kb_search/kb_get으로 KB 먼저 조회** 후 답변:
|
|
1725
|
+
- 팀원 정보 → \`domain: team\`
|
|
1726
|
+
- 프로젝트 현황 → \`domain: project\`
|
|
1727
|
+
- 의사결정 기록 → \`domain: decision\`
|
|
1728
|
+
- 업무 프로세스 → \`domain: process\`
|
|
1729
|
+
- 인프라 구성 → \`domain: infra\`
|
|
1730
|
+
- KPI → \`domain: kpi\`
|
|
1972
1731
|
|
|
1973
|
-
|
|
1732
|
+
**금지:** 위 주제를 자체 지식/세션 기억만으로 답변하는 것.
|
|
1733
|
+
KB에 없으면: "KB에 해당 정보가 없습니다. 알려주시면 등록하겠습니다."
|
|
1974
1734
|
|
|
1975
|
-
|
|
1735
|
+
### 쓰기 (Write-Back)
|
|
1736
|
+
다음 상황 → **반드시 kb_upsert로 KB에 즉시 기록:**
|
|
1737
|
+
- 사용자가 팀 정보를 정정하거나 새 사실을 알려줄 때
|
|
1738
|
+
- 의사결정이 내려졌을 때
|
|
1739
|
+
- 프로세스/규칙이 변경되었을 때
|
|
1976
1740
|
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
<!-- 예: ESLint airbnb, 함수형 컴포넌트 필수, any 금지 -->
|
|
1980
|
-
|
|
1981
|
-
### 아키텍처 특이사항
|
|
1982
|
-
|
|
1983
|
-
<!-- 예: DB 직접 접근 금지 — 반드시 API route 통해야 함 -->
|
|
1741
|
+
**금지:** "알겠습니다/기억하겠습니다"만 하고 KB에 쓰지 않는 것.
|
|
1984
1742
|
|
|
1985
1743
|
---
|
|
1986
1744
|
|
|
1987
|
-
## Quality Gate
|
|
1745
|
+
## Pre-Commit Quality Gate
|
|
1988
1746
|
|
|
1989
|
-
|
|
1747
|
+
> **코드 변경이 포함된 커밋 전 반드시 Quality Gate를 통과해야 합니다.**
|
|
1990
1748
|
|
|
1991
1749
|
\`\`\`bash
|
|
1992
|
-
npm run lint
|
|
1993
|
-
npx tsc --noEmit
|
|
1994
|
-
npm run build
|
|
1750
|
+
npm run lint # ESLint 검사
|
|
1751
|
+
npx tsc --noEmit # TypeScript 타입 체크
|
|
1752
|
+
npm run build # 빌드 검증
|
|
1995
1753
|
\`\`\`
|
|
1996
1754
|
|
|
1997
|
-
\`--no-verify\` 사용 금지.
|
|
1755
|
+
\`--no-verify\` 플래그 사용 금지.
|
|
1998
1756
|
|
|
1999
1757
|
---
|
|
2000
1758
|
|
|
2001
|
-
##
|
|
1759
|
+
## 환경변수 (\`~/.semo.env\`)
|
|
2002
1760
|
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
| \`/SEMO:help\` | 도움말 |
|
|
2006
|
-
| \`/SEMO:feedback\` | 피드백 제출 |
|
|
2007
|
-
| \`/SEMO:health\` | 환경 헬스체크 |
|
|
1761
|
+
SEMO는 \`~/.semo.env\` 파일에서 팀 공통 환경변수를 로드합니다.
|
|
1762
|
+
SessionStart 훅과 OpenClaw 게이트웨이 래퍼에서 자동 source됩니다.
|
|
2008
1763
|
|
|
2009
|
-
|
|
1764
|
+
| 변수 | 용도 | 필수 |
|
|
1765
|
+
|------|------|------|
|
|
1766
|
+
| \`DATABASE_URL\` | 팀 Core DB (PostgreSQL) 연결 | ✅ |
|
|
1767
|
+
| \`OPENAI_API_KEY\` | KB 임베딩용 (text-embedding-3-small) | 선택 |
|
|
1768
|
+
| \`SLACK_WEBHOOK\` | Slack 알림 | 선택 |
|
|
2010
1769
|
|
|
2011
|
-
|
|
1770
|
+
키 갱신이 필요하면 \`~/.semo.env\`를 직접 편집하거나 \`semo onboarding -f\`를 실행하세요.
|
|
2012
1771
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
semo context sync # memory/ 수동 최신화
|
|
2017
|
-
semo bots status # 봇 상태 조회
|
|
2018
|
-
\`\`\`
|
|
1772
|
+
---
|
|
1773
|
+
|
|
1774
|
+
> Generated by SEMO CLI v${VERSION}
|
|
2019
1775
|
`;
|
|
2020
1776
|
fs.writeFileSync(claudeMdPath, claudeMdContent);
|
|
2021
1777
|
console.log(chalk_1.default.green("✓ .claude/CLAUDE.md 생성됨"));
|
|
@@ -2030,7 +1786,7 @@ program
|
|
|
2030
1786
|
console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록\n"));
|
|
2031
1787
|
// Standard 패키지 표시
|
|
2032
1788
|
console.log(chalk_1.default.white.bold("Standard (필수)"));
|
|
2033
|
-
const standardPkgs = ["semo-core", "semo-
|
|
1789
|
+
const standardPkgs = ["semo-core", "semo-agents", "semo-scripts"];
|
|
2034
1790
|
for (const pkg of standardPkgs) {
|
|
2035
1791
|
const isInstalled = fs.existsSync(path.join(semoSystemDir, pkg));
|
|
2036
1792
|
console.log(` ${isInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} ${pkg}`);
|
|
@@ -2063,7 +1819,6 @@ program
|
|
|
2063
1819
|
console.log(chalk_1.default.white.bold("Standard:"));
|
|
2064
1820
|
const standardChecks = [
|
|
2065
1821
|
{ name: "semo-core", path: path.join(semoSystemDir, "semo-core") },
|
|
2066
|
-
{ name: "semo-skills", path: path.join(semoSystemDir, "semo-skills") },
|
|
2067
1822
|
];
|
|
2068
1823
|
let standardOk = true;
|
|
2069
1824
|
for (const check of standardChecks) {
|
|
@@ -2101,11 +1856,27 @@ program
|
|
|
2101
1856
|
.command("update")
|
|
2102
1857
|
.description("SEMO를 최신 버전으로 업데이트합니다")
|
|
2103
1858
|
.option("--self", "CLI만 업데이트")
|
|
1859
|
+
.option("--global", "글로벌 스킬/커맨드/에이전트를 DB 최신으로 갱신 (~/.claude/)")
|
|
2104
1860
|
.option("--system", "semo-system만 업데이트")
|
|
2105
1861
|
.option("--skip-cli", "CLI 업데이트 건너뛰기")
|
|
2106
|
-
.option("--only <packages>", "특정 패키지만 업데이트 (쉼표 구분: semo-core,
|
|
1862
|
+
.option("--only <packages>", "특정 패키지만 업데이트 (쉼표 구분: semo-core,biz/management)")
|
|
2107
1863
|
.option("--migrate", "레거시 환경 강제 마이그레이션")
|
|
2108
1864
|
.action(async (options) => {
|
|
1865
|
+
// === --global: 글로벌 스킬 갱신 ===
|
|
1866
|
+
if (options.global) {
|
|
1867
|
+
console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 글로벌 업데이트\n"));
|
|
1868
|
+
console.log(chalk_1.default.gray(" 대상: ~/.claude/skills, commands, agents (DB 최신)\n"));
|
|
1869
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
1870
|
+
if (!connected) {
|
|
1871
|
+
console.log(chalk_1.default.red(" DB 연결 실패 — ~/.semo.env를 확인하세요."));
|
|
1872
|
+
await (0, database_1.closeConnection)();
|
|
1873
|
+
process.exit(1);
|
|
1874
|
+
}
|
|
1875
|
+
await setupStandardGlobal();
|
|
1876
|
+
await (0, database_1.closeConnection)();
|
|
1877
|
+
console.log(chalk_1.default.green.bold("\n✅ 글로벌 스킬 업데이트 완료!\n"));
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
2109
1880
|
console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 업데이트\n"));
|
|
2110
1881
|
const cwd = process.cwd();
|
|
2111
1882
|
const semoSystemDir = path.join(cwd, "semo-system");
|
|
@@ -2159,22 +1930,19 @@ program
|
|
|
2159
1930
|
}
|
|
2160
1931
|
// 업데이트 대상 결정
|
|
2161
1932
|
const updateSemoCore = !isSelectiveUpdate || onlyPackages.includes("semo-core");
|
|
2162
|
-
const updateSemoSkills = !isSelectiveUpdate || onlyPackages.includes("semo-skills");
|
|
2163
1933
|
const updateSemoAgents = !isSelectiveUpdate || onlyPackages.includes("semo-agents");
|
|
2164
1934
|
const updateSemoScripts = !isSelectiveUpdate || onlyPackages.includes("semo-scripts");
|
|
2165
1935
|
console.log(chalk_1.default.cyan("\n📚 semo-system 업데이트"));
|
|
2166
1936
|
console.log(chalk_1.default.gray(" 대상:"));
|
|
2167
1937
|
if (updateSemoCore)
|
|
2168
1938
|
console.log(chalk_1.default.gray(" - semo-core"));
|
|
2169
|
-
if (updateSemoSkills)
|
|
2170
|
-
console.log(chalk_1.default.gray(" - semo-skills"));
|
|
2171
1939
|
if (updateSemoAgents)
|
|
2172
1940
|
console.log(chalk_1.default.gray(" - semo-agents"));
|
|
2173
1941
|
if (updateSemoScripts)
|
|
2174
1942
|
console.log(chalk_1.default.gray(" - semo-scripts"));
|
|
2175
|
-
if (!updateSemoCore && !
|
|
1943
|
+
if (!updateSemoCore && !updateSemoAgents && !updateSemoScripts) {
|
|
2176
1944
|
console.log(chalk_1.default.yellow("\n ⚠️ 업데이트할 패키지가 없습니다."));
|
|
2177
|
-
console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-
|
|
1945
|
+
console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-agents, semo-scripts"));
|
|
2178
1946
|
return;
|
|
2179
1947
|
}
|
|
2180
1948
|
const spinner = (0, ora_1.default)("\n 최신 버전 다운로드 중...").start();
|
|
@@ -2185,7 +1953,6 @@ program
|
|
|
2185
1953
|
// Standard 업데이트 (선택적) - semo-system/ 하위에서 복사
|
|
2186
1954
|
const standardUpdates = [
|
|
2187
1955
|
{ flag: updateSemoCore, name: "semo-core" },
|
|
2188
|
-
{ flag: updateSemoSkills, name: "semo-skills" },
|
|
2189
1956
|
{ flag: updateSemoAgents, name: "semo-agents" },
|
|
2190
1957
|
{ flag: updateSemoScripts, name: "semo-scripts" },
|
|
2191
1958
|
];
|
|
@@ -2272,7 +2039,7 @@ program
|
|
|
2272
2039
|
}
|
|
2273
2040
|
}
|
|
2274
2041
|
// === 6. Hooks 업데이트 ===
|
|
2275
|
-
await setupHooks(
|
|
2042
|
+
await setupHooks(true);
|
|
2276
2043
|
// === 7. 설치 검증 ===
|
|
2277
2044
|
const verificationResult = verifyInstallation(cwd, []);
|
|
2278
2045
|
printVerificationResult(verificationResult);
|
|
@@ -2331,20 +2098,22 @@ program
|
|
|
2331
2098
|
// === config 명령어 (설치 후 설정 변경) ===
|
|
2332
2099
|
const configCmd = program.command("config").description("SEMO 설정 관리");
|
|
2333
2100
|
configCmd
|
|
2334
|
-
.command("
|
|
2335
|
-
.description("
|
|
2101
|
+
.command("env")
|
|
2102
|
+
.description("SEMO 환경변수 설정 (DATABASE_URL, OPENAI_API_KEY 등)")
|
|
2336
2103
|
.option("--credentials-gist <gistId>", "Private GitHub Gist에서 자동 가져오기")
|
|
2104
|
+
.option("--force", "기존 값도 Gist에서 덮어쓰기")
|
|
2337
2105
|
.action(async (options) => {
|
|
2338
|
-
console.log(chalk_1.default.cyan.bold("\n🔑
|
|
2339
|
-
|
|
2340
|
-
const
|
|
2341
|
-
if (
|
|
2106
|
+
console.log(chalk_1.default.cyan.bold("\n🔑 SEMO 환경변수 설정\n"));
|
|
2107
|
+
const existing = readSemoEnvCreds();
|
|
2108
|
+
const hasExisting = Object.keys(existing).length > 0;
|
|
2109
|
+
if (hasExisting && !options.force) {
|
|
2110
|
+
const keys = Object.keys(existing).join(", ");
|
|
2342
2111
|
const { overwrite } = await inquirer_1.default.prompt([
|
|
2343
2112
|
{
|
|
2344
2113
|
type: "confirm",
|
|
2345
2114
|
name: "overwrite",
|
|
2346
|
-
message: `기존
|
|
2347
|
-
default:
|
|
2115
|
+
message: `기존 설정이 있습니다 (${keys}). Gist에서 없는 키를 보충하시겠습니까?`,
|
|
2116
|
+
default: true,
|
|
2348
2117
|
},
|
|
2349
2118
|
]);
|
|
2350
2119
|
if (!overwrite) {
|
|
@@ -2352,34 +2121,8 @@ configCmd
|
|
|
2352
2121
|
return;
|
|
2353
2122
|
}
|
|
2354
2123
|
}
|
|
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
|
-
}
|
|
2124
|
+
await setupSemoEnv(options.credentialsGist, options.force);
|
|
2125
|
+
console.log(chalk_1.default.gray(" 다음 Claude Code 세션부터 자동으로 적용됩니다."));
|
|
2383
2126
|
});
|
|
2384
2127
|
// === doctor 명령어 (설치 상태 진단) ===
|
|
2385
2128
|
program
|
|
@@ -2410,7 +2153,7 @@ program
|
|
|
2410
2153
|
console.log(chalk_1.default.gray(" 💡 해결: semo init 실행"));
|
|
2411
2154
|
}
|
|
2412
2155
|
else {
|
|
2413
|
-
const packages = ["semo-core", "semo-
|
|
2156
|
+
const packages = ["semo-core", "semo-agents", "semo-scripts"];
|
|
2414
2157
|
for (const pkg of packages) {
|
|
2415
2158
|
const pkgPath = path.join(semoSystemDir, pkg);
|
|
2416
2159
|
if (fs.existsSync(pkgPath)) {
|
|
@@ -2484,19 +2227,7 @@ function readSyncState(cwd) {
|
|
|
2484
2227
|
}
|
|
2485
2228
|
catch { /* */ }
|
|
2486
2229
|
}
|
|
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";
|
|
2230
|
+
return { lastPull: null, lastPush: null, sharedCount: 0 };
|
|
2500
2231
|
}
|
|
2501
2232
|
const kbCmd = program
|
|
2502
2233
|
.command("kb")
|
|
@@ -2504,16 +2235,14 @@ const kbCmd = program
|
|
|
2504
2235
|
kbCmd
|
|
2505
2236
|
.command("pull")
|
|
2506
2237
|
.description("DB에서 KB를 로컬 .kb/로 내려받기")
|
|
2507
|
-
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2508
2238
|
.option("--domain <name>", "특정 도메인만")
|
|
2509
2239
|
.action(async (options) => {
|
|
2510
2240
|
const spinner = (0, ora_1.default)("KB 데이터 가져오는 중...").start();
|
|
2511
2241
|
try {
|
|
2512
2242
|
const pool = (0, database_1.getPool)();
|
|
2513
|
-
const result = await (0, kb_1.kbPull)(pool, options.
|
|
2243
|
+
const result = await (0, kb_1.kbPull)(pool, options.domain, process.cwd());
|
|
2514
2244
|
spinner.succeed(`KB pull 완료`);
|
|
2515
|
-
console.log(chalk_1.default.green(` 📦
|
|
2516
|
-
console.log(chalk_1.default.green(` 🤖 봇 KB (${options.bot}): ${result.bot.length}건`));
|
|
2245
|
+
console.log(chalk_1.default.green(` 📦 KB: ${result.length}건`));
|
|
2517
2246
|
// Also pull ontology
|
|
2518
2247
|
const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
|
|
2519
2248
|
console.log(chalk_1.default.green(` 📐 온톨로지: ${ontoCount}개 도메인`));
|
|
@@ -2529,9 +2258,8 @@ kbCmd
|
|
|
2529
2258
|
kbCmd
|
|
2530
2259
|
.command("push")
|
|
2531
2260
|
.description("로컬 .kb/ 데이터를 DB에 업로드")
|
|
2532
|
-
.option("--
|
|
2533
|
-
.option("--
|
|
2534
|
-
.option("--file <path>", ".kb/ 내 특정 파일")
|
|
2261
|
+
.option("--file <path>", ".kb/ 내 특정 파일", "team.json")
|
|
2262
|
+
.option("--created-by <name>", "작성자 식별자")
|
|
2535
2263
|
.action(async (options) => {
|
|
2536
2264
|
const cwd = process.cwd();
|
|
2537
2265
|
const kbDir = path.join(cwd, ".kb");
|
|
@@ -2542,14 +2270,13 @@ kbCmd
|
|
|
2542
2270
|
const spinner = (0, ora_1.default)("KB 데이터 업로드 중...").start();
|
|
2543
2271
|
try {
|
|
2544
2272
|
const pool = (0, database_1.getPool)();
|
|
2545
|
-
const
|
|
2546
|
-
const filePath = path.join(kbDir, filename);
|
|
2273
|
+
const filePath = path.join(kbDir, options.file);
|
|
2547
2274
|
if (!fs.existsSync(filePath)) {
|
|
2548
|
-
spinner.fail(`파일을 찾을 수 없습니다: .kb/${
|
|
2275
|
+
spinner.fail(`파일을 찾을 수 없습니다: .kb/${options.file}`);
|
|
2549
2276
|
process.exit(1);
|
|
2550
2277
|
}
|
|
2551
2278
|
const entries = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2552
|
-
const result = await (0, kb_1.kbPush)(pool,
|
|
2279
|
+
const result = await (0, kb_1.kbPush)(pool, entries, options.createdBy, cwd);
|
|
2553
2280
|
spinner.succeed(`KB push 완료`);
|
|
2554
2281
|
console.log(chalk_1.default.green(` ✅ ${result.upserted}건 업서트됨`));
|
|
2555
2282
|
if (result.errors.length > 0) {
|
|
@@ -2567,15 +2294,14 @@ kbCmd
|
|
|
2567
2294
|
kbCmd
|
|
2568
2295
|
.command("status")
|
|
2569
2296
|
.description("KB 동기화 상태 확인")
|
|
2570
|
-
.
|
|
2571
|
-
.action(async (options) => {
|
|
2297
|
+
.action(async () => {
|
|
2572
2298
|
const spinner = (0, ora_1.default)("KB 상태 조회 중...").start();
|
|
2573
2299
|
try {
|
|
2574
2300
|
const pool = (0, database_1.getPool)();
|
|
2575
|
-
const status = await (0, kb_1.kbStatus)(pool
|
|
2301
|
+
const status = await (0, kb_1.kbStatus)(pool);
|
|
2576
2302
|
spinner.stop();
|
|
2577
2303
|
console.log(chalk_1.default.cyan.bold("\n📊 KB 상태\n"));
|
|
2578
|
-
console.log(chalk_1.default.white(" 📦
|
|
2304
|
+
console.log(chalk_1.default.white(" 📦 KB (knowledge_base)"));
|
|
2579
2305
|
console.log(chalk_1.default.gray(` 총 ${status.shared.total}건`));
|
|
2580
2306
|
for (const [domain, count] of Object.entries(status.shared.domains)) {
|
|
2581
2307
|
console.log(chalk_1.default.gray(` - ${domain}: ${count}건`));
|
|
@@ -2583,17 +2309,6 @@ kbCmd
|
|
|
2583
2309
|
if (status.shared.lastUpdated) {
|
|
2584
2310
|
console.log(chalk_1.default.gray(` 최종 업데이트: ${status.shared.lastUpdated}`));
|
|
2585
2311
|
}
|
|
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
2312
|
// Local sync state
|
|
2598
2313
|
const syncState = readSyncState(process.cwd());
|
|
2599
2314
|
if (syncState.lastPull || syncState.lastPush) {
|
|
@@ -2615,42 +2330,30 @@ kbCmd
|
|
|
2615
2330
|
kbCmd
|
|
2616
2331
|
.command("list")
|
|
2617
2332
|
.description("KB 항목 목록 조회")
|
|
2618
|
-
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2619
2333
|
.option("--domain <name>", "도메인 필터")
|
|
2620
2334
|
.option("--limit <n>", "최대 항목 수", "50")
|
|
2621
2335
|
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
2622
2336
|
.action(async (options) => {
|
|
2623
2337
|
try {
|
|
2624
2338
|
const pool = (0, database_1.getPool)();
|
|
2625
|
-
const
|
|
2339
|
+
const entries = await (0, kb_1.kbList)(pool, {
|
|
2626
2340
|
domain: options.domain,
|
|
2627
|
-
botId: options.bot,
|
|
2628
2341
|
limit: parseInt(options.limit),
|
|
2629
2342
|
});
|
|
2630
2343
|
if (options.format === "json") {
|
|
2631
|
-
console.log(JSON.stringify(
|
|
2344
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
2632
2345
|
}
|
|
2633
2346
|
else {
|
|
2634
2347
|
console.log(chalk_1.default.cyan.bold("\n📋 KB 목록\n"));
|
|
2635
|
-
if (
|
|
2636
|
-
console.log(chalk_1.default.white(" 📦 공통 KB"));
|
|
2637
|
-
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
2638
|
-
for (const entry of result.shared) {
|
|
2639
|
-
const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
|
|
2640
|
-
console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
|
|
2641
|
-
console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
|
|
2642
|
-
}
|
|
2643
|
-
}
|
|
2644
|
-
if (result.bot.length > 0) {
|
|
2645
|
-
console.log(chalk_1.default.white(`\n 🤖 봇 KB (${options.bot})`));
|
|
2348
|
+
if (entries.length > 0) {
|
|
2646
2349
|
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
2647
|
-
for (const entry of
|
|
2350
|
+
for (const entry of entries) {
|
|
2648
2351
|
const preview = entry.content.substring(0, 60).replace(/\n/g, " ");
|
|
2649
2352
|
console.log(chalk_1.default.cyan(` [${entry.domain}] `) + chalk_1.default.white(entry.key));
|
|
2650
2353
|
console.log(chalk_1.default.gray(` ${preview}${entry.content.length > 60 ? "..." : ""}`));
|
|
2651
2354
|
}
|
|
2652
2355
|
}
|
|
2653
|
-
|
|
2356
|
+
else {
|
|
2654
2357
|
console.log(chalk_1.default.yellow(" KB가 비어있습니다."));
|
|
2655
2358
|
}
|
|
2656
2359
|
console.log();
|
|
@@ -2666,7 +2369,6 @@ kbCmd
|
|
|
2666
2369
|
kbCmd
|
|
2667
2370
|
.command("search <query>")
|
|
2668
2371
|
.description("KB 검색 (시맨틱 + 텍스트 하이브리드)")
|
|
2669
|
-
.option("--bot <name>", "봇 KB도 검색", detectBotId())
|
|
2670
2372
|
.option("--domain <name>", "도메인 필터")
|
|
2671
2373
|
.option("--limit <n>", "최대 결과 수", "10")
|
|
2672
2374
|
.option("--mode <type>", "검색 모드 (hybrid|semantic|text)", "hybrid")
|
|
@@ -2676,7 +2378,6 @@ kbCmd
|
|
|
2676
2378
|
const pool = (0, database_1.getPool)();
|
|
2677
2379
|
const results = await (0, kb_1.kbSearch)(pool, query, {
|
|
2678
2380
|
domain: options.domain,
|
|
2679
|
-
botId: options.bot,
|
|
2680
2381
|
limit: parseInt(options.limit),
|
|
2681
2382
|
mode: options.mode,
|
|
2682
2383
|
});
|
|
@@ -2703,46 +2404,9 @@ kbCmd
|
|
|
2703
2404
|
process.exit(1);
|
|
2704
2405
|
}
|
|
2705
2406
|
});
|
|
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
2407
|
kbCmd
|
|
2743
2408
|
.command("embed")
|
|
2744
2409
|
.description("기존 KB 항목에 임베딩 벡터 생성 (OPENAI_API_KEY 필요)")
|
|
2745
|
-
.option("--bot <name>", "봇 KB도 임베딩", detectBotId())
|
|
2746
2410
|
.option("--domain <name>", "도메인 필터")
|
|
2747
2411
|
.option("--force", "이미 임베딩된 항목도 재생성")
|
|
2748
2412
|
.action(async (options) => {
|
|
@@ -2755,29 +2419,17 @@ kbCmd
|
|
|
2755
2419
|
try {
|
|
2756
2420
|
const pool = (0, database_1.getPool)();
|
|
2757
2421
|
const client = await pool.connect();
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
const sharedParams = [];
|
|
2422
|
+
let sql = "SELECT kb_id, domain, key, content FROM semo.knowledge_base WHERE 1=1";
|
|
2423
|
+
const params = [];
|
|
2761
2424
|
let pIdx = 1;
|
|
2762
2425
|
if (!options.force)
|
|
2763
|
-
|
|
2426
|
+
sql += " AND embedding IS NULL";
|
|
2764
2427
|
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);
|
|
2428
|
+
sql += ` AND domain = $${pIdx++}`;
|
|
2429
|
+
params.push(options.domain);
|
|
2778
2430
|
}
|
|
2779
|
-
const
|
|
2780
|
-
const total =
|
|
2431
|
+
const rows = await client.query(sql, params);
|
|
2432
|
+
const total = rows.rows.length;
|
|
2781
2433
|
spinner.succeed(`${total}건 임베딩 대상`);
|
|
2782
2434
|
if (total === 0) {
|
|
2783
2435
|
console.log(chalk_1.default.green(" 모든 항목이 이미 임베딩되어 있습니다."));
|
|
@@ -2787,8 +2439,7 @@ kbCmd
|
|
|
2787
2439
|
}
|
|
2788
2440
|
let done = 0;
|
|
2789
2441
|
const embedSpinner = (0, ora_1.default)(`임베딩 생성 중... 0/${total}`).start();
|
|
2790
|
-
|
|
2791
|
-
for (const row of sharedRows.rows) {
|
|
2442
|
+
for (const row of rows.rows) {
|
|
2792
2443
|
const embedding = await (0, kb_1.generateEmbedding)(`${row.key}: ${row.content}`);
|
|
2793
2444
|
if (embedding) {
|
|
2794
2445
|
await client.query("UPDATE semo.knowledge_base SET embedding = $1::vector WHERE kb_id = $2", [`[${embedding.join(",")}]`, row.kb_id]);
|
|
@@ -2796,15 +2447,6 @@ kbCmd
|
|
|
2796
2447
|
done++;
|
|
2797
2448
|
embedSpinner.text = `임베딩 생성 중... ${done}/${total}`;
|
|
2798
2449
|
}
|
|
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
2450
|
embedSpinner.succeed(`${done}건 임베딩 완료`);
|
|
2809
2451
|
client.release();
|
|
2810
2452
|
await (0, database_1.closeConnection)();
|
|
@@ -2818,7 +2460,6 @@ kbCmd
|
|
|
2818
2460
|
kbCmd
|
|
2819
2461
|
.command("sync")
|
|
2820
2462
|
.description("양방향 동기화 (pull → merge → push)")
|
|
2821
|
-
.option("--bot <name>", "봇 ID", detectBotId())
|
|
2822
2463
|
.option("--domain <name>", "도메인 필터")
|
|
2823
2464
|
.action(async (options) => {
|
|
2824
2465
|
console.log(chalk_1.default.cyan.bold("\n🔄 KB 동기화\n"));
|
|
@@ -2826,8 +2467,8 @@ kbCmd
|
|
|
2826
2467
|
try {
|
|
2827
2468
|
const pool = (0, database_1.getPool)();
|
|
2828
2469
|
// Step 1: Pull
|
|
2829
|
-
const pulled = await (0, kb_1.kbPull)(pool, options.
|
|
2830
|
-
spinner.succeed(`Pull 완료:
|
|
2470
|
+
const pulled = await (0, kb_1.kbPull)(pool, options.domain, process.cwd());
|
|
2471
|
+
spinner.succeed(`Pull 완료: ${pulled.length}건`);
|
|
2831
2472
|
// Step 2: Pull ontology
|
|
2832
2473
|
const spinner2 = (0, ora_1.default)("Step 2/2: 온톨로지 동기화...").start();
|
|
2833
2474
|
const ontoCount = await (0, kb_1.ontoPullToLocal)(pool, process.cwd());
|
|
@@ -2957,6 +2598,7 @@ ontoCmd
|
|
|
2957
2598
|
(0, bots_1.registerBotsCommands)(program);
|
|
2958
2599
|
(0, get_1.registerGetCommands)(program);
|
|
2959
2600
|
(0, sessions_1.registerSessionsCommands)(program);
|
|
2601
|
+
(0, db_1.registerDbCommands)(program);
|
|
2960
2602
|
// === semo skills — DB 시딩 ===
|
|
2961
2603
|
/**
|
|
2962
2604
|
* SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
|
|
@@ -2984,119 +2626,12 @@ function parseSkillFrontmatter(content) {
|
|
|
2984
2626
|
// tools 배열
|
|
2985
2627
|
const toolsMatch = fm.match(/^tools:\s*\[(.+)\]$/m);
|
|
2986
2628
|
const tools = toolsMatch ? toolsMatch[1].split(",").map((t) => t.trim()) : [];
|
|
2987
|
-
// category
|
|
2629
|
+
// category
|
|
2988
2630
|
const category = "core";
|
|
2989
2631
|
return { name, description, category, tools };
|
|
2990
2632
|
}
|
|
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
|
-
});
|
|
2633
|
+
// semo skills seed — 제거됨 (중앙 DB 단일 SoT)
|
|
2634
|
+
// 봇 전용 스킬은 semo bots seed로 동기화
|
|
3100
2635
|
// === -v 옵션 처리 (program.parse 전에 직접 처리) ===
|
|
3101
2636
|
async function main() {
|
|
3102
2637
|
const args = process.argv.slice(2);
|