@team-semicolon/semo-cli 4.12.0 → 4.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/commitments.js +25 -3
- package/dist/commands/commitments.test.d.ts +9 -0
- package/dist/commands/commitments.test.js +223 -0
- package/dist/commands/context.js +23 -3
- package/dist/commands/harness.d.ts +8 -0
- package/dist/commands/harness.js +412 -0
- package/dist/commands/incubator.d.ts +10 -0
- package/dist/commands/incubator.js +517 -0
- package/dist/commands/service.d.ts +10 -0
- package/dist/commands/service.js +283 -0
- package/dist/commands/sessions.js +156 -0
- package/dist/commands/skill-sync.d.ts +2 -1
- package/dist/commands/skill-sync.js +88 -23
- package/dist/commands/skill-sync.test.js +78 -45
- package/dist/database.d.ts +2 -1
- package/dist/database.js +109 -27
- package/dist/global-cache.js +26 -14
- package/dist/index.js +578 -522
- package/dist/kb.d.ts +4 -4
- package/dist/kb.js +211 -101
- package/dist/semo-workspace.js +51 -0
- package/dist/service-migrate.d.ts +114 -0
- package/dist/service-migrate.js +457 -0
- package/dist/templates/harness/commit-msg +1 -0
- package/dist/templates/harness/commitlint.config.js +11 -0
- package/dist/templates/harness/eslint.config.mjs +17 -0
- package/dist/templates/harness/pr-quality-gate.yml +19 -0
- package/dist/templates/harness/pre-commit +1 -0
- package/dist/templates/harness/pre-push +1 -0
- package/dist/templates/harness/prettierignore +5 -0
- package/dist/templates/harness/prettierrc.json +7 -0
- package/package.json +8 -4
|
@@ -56,8 +56,8 @@ let tmpDir;
|
|
|
56
56
|
let passed = 0;
|
|
57
57
|
let failed = 0;
|
|
58
58
|
function setup() {
|
|
59
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(),
|
|
60
|
-
fs.mkdirSync(path.join(dir,
|
|
59
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-sync-test-'));
|
|
60
|
+
fs.mkdirSync(path.join(dir, 'bot-workspaces'), { recursive: true });
|
|
61
61
|
return dir;
|
|
62
62
|
}
|
|
63
63
|
function cleanup(dir) {
|
|
@@ -73,114 +73,147 @@ function assert(condition, message) {
|
|
|
73
73
|
console.log(` ❌ ${message}`);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
function makeBotSkill(dir, botId, skillName, content) {
|
|
77
|
-
const skillDir = path.join(dir,
|
|
76
|
+
function makeBotSkill(dir, botId, skillName, content, refs) {
|
|
77
|
+
const skillDir = path.join(dir, 'bot-workspaces', botId, 'skills', skillName);
|
|
78
78
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
79
|
-
fs.writeFileSync(path.join(skillDir,
|
|
79
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
80
|
+
if (refs) {
|
|
81
|
+
const refsDir = path.join(skillDir, 'references');
|
|
82
|
+
fs.mkdirSync(refsDir, { recursive: true });
|
|
83
|
+
for (const [name, refContent] of Object.entries(refs)) {
|
|
84
|
+
fs.writeFileSync(path.join(refsDir, name), refContent);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
80
87
|
}
|
|
81
88
|
// ─── 테스트 ───────────────────────────────────────────────
|
|
82
|
-
console.log(
|
|
89
|
+
console.log('\n🧪 skill-sync.test.ts\n');
|
|
83
90
|
// 1. 빈 디렉토리
|
|
84
|
-
console.log(
|
|
91
|
+
console.log('Case 1: 빈 디렉토리');
|
|
85
92
|
tmpDir = setup();
|
|
86
93
|
{
|
|
87
94
|
const skills = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
88
|
-
assert(skills.length === 0,
|
|
95
|
+
assert(skills.length === 0, 'skills = 0');
|
|
89
96
|
}
|
|
90
97
|
cleanup(tmpDir);
|
|
91
98
|
// 2. 봇 전용 스킬 — flat name
|
|
92
|
-
console.log(
|
|
99
|
+
console.log('Case 2: 봇 전용 스킬 flat name');
|
|
93
100
|
tmpDir = setup();
|
|
94
101
|
{
|
|
95
|
-
makeBotSkill(tmpDir,
|
|
102
|
+
makeBotSkill(tmpDir, 'semiclaw', 'github-issue-pipeline', '# GH Pipeline');
|
|
96
103
|
const skills = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
97
104
|
assert(skills.length === 1, `skills = 1 (got ${skills.length})`);
|
|
98
|
-
assert(skills[0].name ===
|
|
99
|
-
assert(skills[0].botId ===
|
|
100
|
-
assert(skills[0].package ===
|
|
105
|
+
assert(skills[0].name === 'github-issue-pipeline', `name = github-issue-pipeline (got ${skills[0].name})`);
|
|
106
|
+
assert(skills[0].botId === 'semiclaw', 'botId = semiclaw');
|
|
107
|
+
assert(skills[0].package === 'openclaw', 'package = openclaw');
|
|
101
108
|
}
|
|
102
109
|
cleanup(tmpDir);
|
|
103
110
|
// 3. 복수 봇의 전용 스킬 — botId 정확성
|
|
104
|
-
console.log(
|
|
111
|
+
console.log('Case 3: 복수 봇 botId 정확성');
|
|
105
112
|
tmpDir = setup();
|
|
106
113
|
{
|
|
107
|
-
makeBotSkill(tmpDir,
|
|
108
|
-
makeBotSkill(tmpDir,
|
|
109
|
-
makeBotSkill(tmpDir,
|
|
110
|
-
makeBotSkill(tmpDir,
|
|
114
|
+
makeBotSkill(tmpDir, 'semiclaw', 'skill-a', '# A');
|
|
115
|
+
makeBotSkill(tmpDir, 'semiclaw', 'skill-b', '# B');
|
|
116
|
+
makeBotSkill(tmpDir, 'workclaw', 'skill-c', '# C');
|
|
117
|
+
makeBotSkill(tmpDir, 'infraclaw', 'skill-d', '# D');
|
|
111
118
|
const skills = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
112
119
|
assert(skills.length === 4, `총 4개 (got ${skills.length})`);
|
|
113
|
-
const semiclawSkills = skills.filter((s) => s.botId ===
|
|
114
|
-
const workclawSkills = skills.filter((s) => s.botId ===
|
|
115
|
-
const infraclawSkills = skills.filter((s) => s.botId ===
|
|
120
|
+
const semiclawSkills = skills.filter((s) => s.botId === 'semiclaw');
|
|
121
|
+
const workclawSkills = skills.filter((s) => s.botId === 'workclaw');
|
|
122
|
+
const infraclawSkills = skills.filter((s) => s.botId === 'infraclaw');
|
|
116
123
|
assert(semiclawSkills.length === 2, `semiclaw: 2개 (got ${semiclawSkills.length})`);
|
|
117
124
|
assert(workclawSkills.length === 1, `workclaw: 1개 (got ${workclawSkills.length})`);
|
|
118
125
|
assert(infraclawSkills.length === 1, `infraclaw: 1개 (got ${infraclawSkills.length})`);
|
|
119
126
|
}
|
|
120
127
|
cleanup(tmpDir);
|
|
121
128
|
// 4. .skill 확장자 디렉토리 → 스킵
|
|
122
|
-
console.log(
|
|
129
|
+
console.log('Case 4: .skill 확장자 디렉토리');
|
|
123
130
|
tmpDir = setup();
|
|
124
131
|
{
|
|
125
|
-
const dotSkillDir = path.join(tmpDir,
|
|
132
|
+
const dotSkillDir = path.join(tmpDir, 'bot-workspaces', 'semiclaw', 'skills', 'old-format.skill');
|
|
126
133
|
fs.mkdirSync(dotSkillDir, { recursive: true });
|
|
127
|
-
fs.writeFileSync(path.join(dotSkillDir,
|
|
128
|
-
makeBotSkill(tmpDir,
|
|
134
|
+
fs.writeFileSync(path.join(dotSkillDir, 'SKILL.md'), 'should be skipped');
|
|
135
|
+
makeBotSkill(tmpDir, 'semiclaw', 'valid-skill', '# Valid');
|
|
129
136
|
const skills = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
130
137
|
assert(skills.length === 1, `.skill 디렉토리 스킵, skills = 1 (got ${skills.length})`);
|
|
131
|
-
assert(skills[0].name ===
|
|
138
|
+
assert(skills[0].name === 'valid-skill', '올바른 스킬만 반환');
|
|
132
139
|
}
|
|
133
140
|
cleanup(tmpDir);
|
|
134
141
|
// 5. SKILL.md 없는 디렉토리 → 스킵
|
|
135
|
-
console.log(
|
|
142
|
+
console.log('Case 5: SKILL.md 없는 디렉토리');
|
|
136
143
|
tmpDir = setup();
|
|
137
144
|
{
|
|
138
|
-
const emptyDir = path.join(tmpDir,
|
|
145
|
+
const emptyDir = path.join(tmpDir, 'bot-workspaces', 'semiclaw', 'skills', 'no-skill-md');
|
|
139
146
|
fs.mkdirSync(emptyDir, { recursive: true });
|
|
140
|
-
makeBotSkill(tmpDir,
|
|
147
|
+
makeBotSkill(tmpDir, 'semiclaw', 'valid-skill', 'content');
|
|
141
148
|
const skills = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
142
149
|
assert(skills.length === 1, `SKILL.md 없는 디렉토리 스킵, skills = 1 (got ${skills.length})`);
|
|
143
150
|
}
|
|
144
151
|
cleanup(tmpDir);
|
|
145
152
|
// 6. 스킬 변경 감지 — 파일 수정 후 재스캔
|
|
146
|
-
console.log(
|
|
153
|
+
console.log('Case 6: 스킬 변경 감지');
|
|
147
154
|
tmpDir = setup();
|
|
148
155
|
{
|
|
149
|
-
makeBotSkill(tmpDir,
|
|
156
|
+
makeBotSkill(tmpDir, 'semiclaw', 'review', '# v1 content');
|
|
150
157
|
const scan1 = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
151
|
-
assert(scan1[0].prompt ===
|
|
152
|
-
fs.writeFileSync(path.join(tmpDir,
|
|
158
|
+
assert(scan1[0].prompt === '# v1 content', '초기 스캔: v1');
|
|
159
|
+
fs.writeFileSync(path.join(tmpDir, 'bot-workspaces', 'semiclaw', 'skills', 'review', 'SKILL.md'), '# v2 updated content');
|
|
153
160
|
const scan2 = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
154
|
-
assert(scan2[0].prompt ===
|
|
161
|
+
assert(scan2[0].prompt === '# v2 updated content', '재스캔: v2 반영됨');
|
|
155
162
|
}
|
|
156
163
|
cleanup(tmpDir);
|
|
157
164
|
// 7. 스킬 추가 감지 — 새 디렉토리 추가 후 재스캔
|
|
158
|
-
console.log(
|
|
165
|
+
console.log('Case 7: 스킬 추가 감지');
|
|
159
166
|
tmpDir = setup();
|
|
160
167
|
{
|
|
161
|
-
makeBotSkill(tmpDir,
|
|
168
|
+
makeBotSkill(tmpDir, 'semiclaw', 'existing', '# Existing');
|
|
162
169
|
const scan1 = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
163
|
-
assert(scan1.length === 1,
|
|
164
|
-
makeBotSkill(tmpDir,
|
|
170
|
+
assert(scan1.length === 1, '초기: 1개');
|
|
171
|
+
makeBotSkill(tmpDir, 'semiclaw', 'new-skill', '# New Skill');
|
|
165
172
|
const scan2 = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
166
173
|
assert(scan2.length === 2, `추가 후: 2개 (got ${scan2.length})`);
|
|
167
174
|
}
|
|
168
175
|
cleanup(tmpDir);
|
|
169
176
|
// 8. 스킬 삭제 감지 — SKILL.md 삭제 후 재스캔
|
|
170
|
-
console.log(
|
|
177
|
+
console.log('Case 8: 스킬 삭제 감지');
|
|
171
178
|
tmpDir = setup();
|
|
172
179
|
{
|
|
173
|
-
makeBotSkill(tmpDir,
|
|
174
|
-
makeBotSkill(tmpDir,
|
|
180
|
+
makeBotSkill(tmpDir, 'semiclaw', 'to-delete', '# Will be deleted');
|
|
181
|
+
makeBotSkill(tmpDir, 'semiclaw', 'keep', '# Keep');
|
|
175
182
|
const scan1 = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
176
|
-
assert(scan1.length === 2,
|
|
177
|
-
fs.unlinkSync(path.join(tmpDir,
|
|
183
|
+
assert(scan1.length === 2, '초기: 2개');
|
|
184
|
+
fs.unlinkSync(path.join(tmpDir, 'bot-workspaces', 'semiclaw', 'skills', 'to-delete', 'SKILL.md'));
|
|
178
185
|
const scan2 = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
179
186
|
assert(scan2.length === 1, `삭제 후: 1개 (got ${scan2.length})`);
|
|
180
|
-
assert(scan2[0].name ===
|
|
187
|
+
assert(scan2[0].name === 'keep', '남은 스킬: keep');
|
|
188
|
+
}
|
|
189
|
+
cleanup(tmpDir);
|
|
190
|
+
// 9. references/ 있는 스킬 → referenceFiles 맵 반환
|
|
191
|
+
console.log('Case 9: references/ 있는 스킬');
|
|
192
|
+
tmpDir = setup();
|
|
193
|
+
{
|
|
194
|
+
makeBotSkill(tmpDir, 'semiclaw', 'adhoc-meeting', '# Adhoc Meeting', {
|
|
195
|
+
'meeting-template.md': '# Template',
|
|
196
|
+
'decision-template.md': '# Decision',
|
|
197
|
+
});
|
|
198
|
+
const skills = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
199
|
+
assert(skills.length === 1, 'skills = 1');
|
|
200
|
+
assert(skills[0].referenceFiles !== undefined, 'referenceFiles 존재');
|
|
201
|
+
assert(Object.keys(skills[0].referenceFiles).length === 2, 'referenceFiles 2개');
|
|
202
|
+
assert(skills[0].referenceFiles['meeting-template.md'] === '# Template', 'meeting-template 내용 일치');
|
|
203
|
+
assert(skills[0].referenceFiles['decision-template.md'] === '# Decision', 'decision-template 내용 일치');
|
|
204
|
+
}
|
|
205
|
+
cleanup(tmpDir);
|
|
206
|
+
// 10. references/ 없는 스킬 → referenceFiles undefined
|
|
207
|
+
console.log('Case 10: references/ 없는 스킬');
|
|
208
|
+
tmpDir = setup();
|
|
209
|
+
{
|
|
210
|
+
makeBotSkill(tmpDir, 'semiclaw', 'simple-skill', '# Simple');
|
|
211
|
+
const skills = (0, skill_sync_1.scanSkills)(tmpDir);
|
|
212
|
+
assert(skills.length === 1, 'skills = 1');
|
|
213
|
+
assert(skills[0].referenceFiles === undefined, 'referenceFiles undefined');
|
|
181
214
|
}
|
|
182
215
|
cleanup(tmpDir);
|
|
183
216
|
// ─── 결과 ──────────────────────────────────────────────────
|
|
184
|
-
console.log(`\n${
|
|
217
|
+
console.log(`\n${'─'.repeat(40)}`);
|
|
185
218
|
console.log(`총 ${passed + failed}개 테스트: ✅ ${passed} passed, ❌ ${failed} failed\n`);
|
|
186
219
|
process.exit(failed > 0 ? 1 : 0);
|
package/dist/database.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - command_definitions (prompt as content)
|
|
12
12
|
* - semo.skills / semo.agents / semo.commands 는 하위 호환 뷰
|
|
13
13
|
*/
|
|
14
|
-
import { Pool } from
|
|
14
|
+
import { Pool } from 'pg';
|
|
15
15
|
export declare function getPool(): Pool;
|
|
16
16
|
export interface Skill {
|
|
17
17
|
id: string;
|
|
@@ -20,6 +20,7 @@ export interface Skill {
|
|
|
20
20
|
description: string | null;
|
|
21
21
|
content: string;
|
|
22
22
|
bot_ids: string[];
|
|
23
|
+
reference_files?: Record<string, string>;
|
|
23
24
|
category: string;
|
|
24
25
|
package: string;
|
|
25
26
|
is_active: boolean;
|
package/dist/database.js
CHANGED
|
@@ -69,11 +69,11 @@ const env_parser_1 = require("./env-parser");
|
|
|
69
69
|
// 인터랙티브 쉘이 아닌 환경에서 환경변수를 공급한다.
|
|
70
70
|
// 이미 설정된 환경변수는 덮어쓰지 않는다 (env var > file).
|
|
71
71
|
function loadSemoEnv() {
|
|
72
|
-
const envFile = path.join(os.homedir(),
|
|
72
|
+
const envFile = path.join(os.homedir(), '.claude', 'semo', '.env');
|
|
73
73
|
if (!fs.existsSync(envFile))
|
|
74
74
|
return;
|
|
75
75
|
try {
|
|
76
|
-
const creds = (0, env_parser_1.parseEnvContent)(fs.readFileSync(envFile,
|
|
76
|
+
const creds = (0, env_parser_1.parseEnvContent)(fs.readFileSync(envFile, 'utf8'));
|
|
77
77
|
for (const [key, val] of Object.entries(creds)) {
|
|
78
78
|
if (!process.env[key])
|
|
79
79
|
process.env[key] = val;
|
|
@@ -91,20 +91,22 @@ function buildDbConfig() {
|
|
|
91
91
|
if (process.env.DATABASE_URL) {
|
|
92
92
|
return {
|
|
93
93
|
connectionString: process.env.DATABASE_URL,
|
|
94
|
-
ssl: process.env.DATABASE_URL.includes(
|
|
94
|
+
ssl: process.env.DATABASE_URL.includes('sslmode=require')
|
|
95
|
+
? { rejectUnauthorized: false }
|
|
96
|
+
: false,
|
|
95
97
|
connectionTimeoutMillis: 5000,
|
|
96
98
|
idleTimeoutMillis: 30000,
|
|
97
99
|
};
|
|
98
100
|
}
|
|
99
101
|
if (!process.env.SEMO_DB_HOST && !process.env.DATABASE_URL) {
|
|
100
|
-
throw new Error(
|
|
102
|
+
throw new Error('DB 연결 정보가 없습니다. DATABASE_URL 또는 SEMO_DB_HOST 환경변수를 설정하세요.');
|
|
101
103
|
}
|
|
102
104
|
return {
|
|
103
105
|
host: process.env.SEMO_DB_HOST,
|
|
104
|
-
port: parseInt(process.env.SEMO_DB_PORT ||
|
|
105
|
-
user: process.env.SEMO_DB_USER ||
|
|
106
|
+
port: parseInt(process.env.SEMO_DB_PORT || '5432'),
|
|
107
|
+
user: process.env.SEMO_DB_USER || 'app',
|
|
106
108
|
password: process.env.SEMO_DB_PASSWORD,
|
|
107
|
-
database: process.env.SEMO_DB_NAME ||
|
|
109
|
+
database: process.env.SEMO_DB_NAME || 'appdb',
|
|
108
110
|
ssl: false,
|
|
109
111
|
connectionTimeoutMillis: 5000,
|
|
110
112
|
idleTimeoutMillis: 30000,
|
|
@@ -125,7 +127,7 @@ async function checkDbConnection() {
|
|
|
125
127
|
return dbAvailable;
|
|
126
128
|
try {
|
|
127
129
|
const client = await getPool().connect();
|
|
128
|
-
await client.query(
|
|
130
|
+
await client.query('SELECT 1');
|
|
129
131
|
client.release();
|
|
130
132
|
dbAvailable = true;
|
|
131
133
|
}
|
|
@@ -140,17 +142,95 @@ async function checkDbConnection() {
|
|
|
140
142
|
const FALLBACK_SKILLS = [];
|
|
141
143
|
const FALLBACK_COMMANDS = [];
|
|
142
144
|
const FALLBACK_AGENTS = [
|
|
143
|
-
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
{
|
|
146
|
+
id: 'agent-semiclaw',
|
|
147
|
+
name: 'semiclaw',
|
|
148
|
+
display_name: 'SemiClaw 🦀',
|
|
149
|
+
content: '',
|
|
150
|
+
package: 'openclaw',
|
|
151
|
+
is_active: true,
|
|
152
|
+
install_order: 1,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'agent-workclaw',
|
|
156
|
+
name: 'workclaw',
|
|
157
|
+
display_name: 'WorkClaw 🛠️',
|
|
158
|
+
content: '',
|
|
159
|
+
package: 'openclaw',
|
|
160
|
+
is_active: true,
|
|
161
|
+
install_order: 2,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'agent-reviewclaw',
|
|
165
|
+
name: 'reviewclaw',
|
|
166
|
+
display_name: 'ReviewClaw 🔍',
|
|
167
|
+
content: '',
|
|
168
|
+
package: 'openclaw',
|
|
169
|
+
is_active: true,
|
|
170
|
+
install_order: 3,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'agent-planclaw',
|
|
174
|
+
name: 'planclaw',
|
|
175
|
+
display_name: 'PlanClaw 🗓️',
|
|
176
|
+
content: '',
|
|
177
|
+
package: 'openclaw',
|
|
178
|
+
is_active: true,
|
|
179
|
+
install_order: 4,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'agent-designclaw',
|
|
183
|
+
name: 'designclaw',
|
|
184
|
+
display_name: 'DesignClaw 🎨',
|
|
185
|
+
content: '',
|
|
186
|
+
package: 'openclaw',
|
|
187
|
+
is_active: true,
|
|
188
|
+
install_order: 5,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'agent-infraclaw',
|
|
192
|
+
name: 'infraclaw',
|
|
193
|
+
display_name: 'InfraClaw 🏗️',
|
|
194
|
+
content: '',
|
|
195
|
+
package: 'openclaw',
|
|
196
|
+
is_active: true,
|
|
197
|
+
install_order: 6,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'agent-growthclaw',
|
|
201
|
+
name: 'growthclaw',
|
|
202
|
+
display_name: 'GrowthClaw 🌱',
|
|
203
|
+
content: '',
|
|
204
|
+
package: 'openclaw',
|
|
205
|
+
is_active: true,
|
|
206
|
+
install_order: 7,
|
|
207
|
+
},
|
|
150
208
|
];
|
|
151
209
|
const FALLBACK_PACKAGES = [
|
|
152
|
-
{
|
|
153
|
-
|
|
210
|
+
{
|
|
211
|
+
id: 'pkg-cli',
|
|
212
|
+
name: 'semo-cli',
|
|
213
|
+
display_name: 'SEMO CLI',
|
|
214
|
+
description: 'CLI 도구 (컨텍스트 동기화, 봇 관리)',
|
|
215
|
+
layer: 'standard',
|
|
216
|
+
package_type: 'standard',
|
|
217
|
+
version: '4.2.0',
|
|
218
|
+
is_active: true,
|
|
219
|
+
is_required: true,
|
|
220
|
+
install_order: 10,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'pkg-dashboard',
|
|
224
|
+
name: 'semo-dashboard',
|
|
225
|
+
display_name: 'SEMO Dashboard',
|
|
226
|
+
description: '봇 상태/KB 대시보드',
|
|
227
|
+
layer: 'standard',
|
|
228
|
+
package_type: 'standard',
|
|
229
|
+
version: '0.1.0',
|
|
230
|
+
is_active: true,
|
|
231
|
+
is_required: false,
|
|
232
|
+
install_order: 30,
|
|
233
|
+
},
|
|
154
234
|
];
|
|
155
235
|
// ============================================================
|
|
156
236
|
// DB 조회 함수
|
|
@@ -162,7 +242,7 @@ const FALLBACK_PACKAGES = [
|
|
|
162
242
|
async function getActiveSkills() {
|
|
163
243
|
const isConnected = await checkDbConnection();
|
|
164
244
|
if (!isConnected) {
|
|
165
|
-
console.warn(
|
|
245
|
+
console.warn('⚠️ DB 연결 실패, 폴백 스킬 목록 사용');
|
|
166
246
|
return FALLBACK_SKILLS.filter((s) => s.is_active);
|
|
167
247
|
}
|
|
168
248
|
try {
|
|
@@ -173,6 +253,7 @@ async function getActiveSkills() {
|
|
|
173
253
|
ARRAY(SELECT jsonb_array_elements_text(metadata->'bot_ids')),
|
|
174
254
|
ARRAY[]::text[]
|
|
175
255
|
) AS bot_ids,
|
|
256
|
+
metadata->'reference_files' AS reference_files,
|
|
176
257
|
category, package, is_active, is_required, install_order, version
|
|
177
258
|
FROM semo.skill_definitions
|
|
178
259
|
WHERE is_active = true AND office_id IS NULL
|
|
@@ -181,7 +262,7 @@ async function getActiveSkills() {
|
|
|
181
262
|
return result.rows;
|
|
182
263
|
}
|
|
183
264
|
catch (error) {
|
|
184
|
-
console.warn(
|
|
265
|
+
console.warn('⚠️ 스킬 조회 실패, 폴백 데이터 사용:', error);
|
|
185
266
|
return FALLBACK_SKILLS.filter((s) => s.is_active);
|
|
186
267
|
}
|
|
187
268
|
}
|
|
@@ -199,7 +280,7 @@ async function getActiveSkillNames() {
|
|
|
199
280
|
async function getActiveSkillsForBot(botId) {
|
|
200
281
|
const isConnected = await checkDbConnection();
|
|
201
282
|
if (!isConnected) {
|
|
202
|
-
console.warn(
|
|
283
|
+
console.warn('⚠️ DB 연결 실패, 폴백 스킬 목록 사용');
|
|
203
284
|
return FALLBACK_SKILLS.filter((s) => s.is_active);
|
|
204
285
|
}
|
|
205
286
|
try {
|
|
@@ -209,6 +290,7 @@ async function getActiveSkillsForBot(botId) {
|
|
|
209
290
|
ARRAY(SELECT jsonb_array_elements_text(sd.metadata->'bot_ids')),
|
|
210
291
|
ARRAY[]::text[]
|
|
211
292
|
) AS bot_ids,
|
|
293
|
+
sd.metadata->'reference_files' AS reference_files,
|
|
212
294
|
sd.category, sd.package, sd.is_active, sd.is_required,
|
|
213
295
|
sd.install_order, sd.version
|
|
214
296
|
FROM semo.skill_definitions sd
|
|
@@ -221,7 +303,7 @@ async function getActiveSkillsForBot(botId) {
|
|
|
221
303
|
return result.rows;
|
|
222
304
|
}
|
|
223
305
|
catch (error) {
|
|
224
|
-
console.warn(
|
|
306
|
+
console.warn('⚠️ 봇 스킬 조회 실패, 폴백 데이터 사용:', error);
|
|
225
307
|
return FALLBACK_SKILLS.filter((s) => s.is_active);
|
|
226
308
|
}
|
|
227
309
|
}
|
|
@@ -232,7 +314,7 @@ async function getActiveSkillsForBot(botId) {
|
|
|
232
314
|
async function getCommands() {
|
|
233
315
|
const isConnected = await checkDbConnection();
|
|
234
316
|
if (!isConnected) {
|
|
235
|
-
console.warn(
|
|
317
|
+
console.warn('⚠️ DB 연결 실패, 폴백 커맨드 목록 사용');
|
|
236
318
|
return FALLBACK_COMMANDS.filter((c) => c.is_active);
|
|
237
319
|
}
|
|
238
320
|
try {
|
|
@@ -246,7 +328,7 @@ async function getCommands() {
|
|
|
246
328
|
return result.rows;
|
|
247
329
|
}
|
|
248
330
|
catch (error) {
|
|
249
|
-
console.warn(
|
|
331
|
+
console.warn('⚠️ 커맨드 조회 실패, 폴백 데이터 사용:', error);
|
|
250
332
|
return FALLBACK_COMMANDS.filter((c) => c.is_active);
|
|
251
333
|
}
|
|
252
334
|
}
|
|
@@ -257,7 +339,7 @@ async function getCommands() {
|
|
|
257
339
|
async function getAgents() {
|
|
258
340
|
const isConnected = await checkDbConnection();
|
|
259
341
|
if (!isConnected) {
|
|
260
|
-
console.warn(
|
|
342
|
+
console.warn('⚠️ DB 연결 실패, 폴백 에이전트 목록 사용');
|
|
261
343
|
return FALLBACK_AGENTS.filter((a) => a.is_active);
|
|
262
344
|
}
|
|
263
345
|
try {
|
|
@@ -273,7 +355,7 @@ async function getAgents() {
|
|
|
273
355
|
return result.rows;
|
|
274
356
|
}
|
|
275
357
|
catch (error) {
|
|
276
|
-
console.warn(
|
|
358
|
+
console.warn('⚠️ 에이전트 조회 실패, 폴백 데이터 사용:', error);
|
|
277
359
|
return FALLBACK_AGENTS.filter((a) => a.is_active);
|
|
278
360
|
}
|
|
279
361
|
}
|
|
@@ -283,7 +365,7 @@ async function getAgents() {
|
|
|
283
365
|
async function getPackages(layer) {
|
|
284
366
|
const isConnected = await checkDbConnection();
|
|
285
367
|
if (!isConnected) {
|
|
286
|
-
console.warn(
|
|
368
|
+
console.warn('⚠️ DB 연결 실패, 폴백 패키지 목록 사용');
|
|
287
369
|
const fallback = FALLBACK_PACKAGES.filter((p) => p.is_active);
|
|
288
370
|
return layer ? fallback.filter((p) => p.layer === layer) : fallback;
|
|
289
371
|
}
|
|
@@ -304,7 +386,7 @@ async function getPackages(layer) {
|
|
|
304
386
|
return result.rows;
|
|
305
387
|
}
|
|
306
388
|
catch (error) {
|
|
307
|
-
console.warn(
|
|
389
|
+
console.warn('⚠️ 패키지 조회 실패, 폴백 데이터 사용:', error);
|
|
308
390
|
const fallback = FALLBACK_PACKAGES.filter((p) => p.is_active);
|
|
309
391
|
return layer ? fallback.filter((p) => p.layer === layer) : fallback;
|
|
310
392
|
}
|
package/dist/global-cache.js
CHANGED
|
@@ -45,7 +45,7 @@ const path = __importStar(require("path"));
|
|
|
45
45
|
const os = __importStar(require("os"));
|
|
46
46
|
const child_process_1 = require("child_process");
|
|
47
47
|
const database_1 = require("./database");
|
|
48
|
-
const isWindows = process.platform ===
|
|
48
|
+
const isWindows = process.platform === 'win32';
|
|
49
49
|
function removeRecursive(targetPath) {
|
|
50
50
|
if (!fs.existsSync(targetPath))
|
|
51
51
|
return;
|
|
@@ -53,10 +53,10 @@ function removeRecursive(targetPath) {
|
|
|
53
53
|
try {
|
|
54
54
|
const stats = fs.lstatSync(targetPath);
|
|
55
55
|
if (stats.isSymbolicLink()) {
|
|
56
|
-
(0, child_process_1.execSync)(`cmd /c "rmdir "${targetPath}""`, { stdio:
|
|
56
|
+
(0, child_process_1.execSync)(`cmd /c "rmdir "${targetPath}""`, { stdio: 'pipe' });
|
|
57
57
|
}
|
|
58
58
|
else {
|
|
59
|
-
(0, child_process_1.execSync)(`cmd /c "rd /s /q "${targetPath}""`, { stdio:
|
|
59
|
+
(0, child_process_1.execSync)(`cmd /c "rd /s /q "${targetPath}""`, { stdio: 'pipe' });
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
catch {
|
|
@@ -64,7 +64,7 @@ function removeRecursive(targetPath) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
else {
|
|
67
|
-
(0, child_process_1.execSync)(`rm -rf "${targetPath}"`, { stdio:
|
|
67
|
+
(0, child_process_1.execSync)(`rm -rf "${targetPath}"`, { stdio: 'pipe' });
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
@@ -90,9 +90,7 @@ function injectAgentInfo(content, botIds) {
|
|
|
90
90
|
const absDescStart = 4 + descIdx;
|
|
91
91
|
const lineEnd = content.indexOf('\n', absDescStart);
|
|
92
92
|
if (lineEnd !== -1) {
|
|
93
|
-
return (content.slice(0, lineEnd) +
|
|
94
|
-
`\n Agents: ${botIds.join(', ')}` +
|
|
95
|
-
content.slice(lineEnd));
|
|
93
|
+
return (content.slice(0, lineEnd) + `\n Agents: ${botIds.join(', ')}` + content.slice(lineEnd));
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
96
|
}
|
|
@@ -104,7 +102,7 @@ function injectAgentInfo(content, botIds) {
|
|
|
104
102
|
return content + agentLine;
|
|
105
103
|
}
|
|
106
104
|
async function syncGlobalCache(claudeDir) {
|
|
107
|
-
const dir = claudeDir || path.join(os.homedir(),
|
|
105
|
+
const dir = claudeDir || path.join(os.homedir(), '.claude');
|
|
108
106
|
fs.mkdirSync(dir, { recursive: true });
|
|
109
107
|
// 병렬 조회
|
|
110
108
|
const [skills, commands, agents, delegations] = await Promise.all([
|
|
@@ -114,7 +112,7 @@ async function syncGlobalCache(claudeDir) {
|
|
|
114
112
|
(0, database_1.getDelegations)(),
|
|
115
113
|
]);
|
|
116
114
|
// 1. 스킬 설치 (전체 교체)
|
|
117
|
-
const skillsDir = path.join(dir,
|
|
115
|
+
const skillsDir = path.join(dir, 'skills');
|
|
118
116
|
removeRecursive(skillsDir);
|
|
119
117
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
120
118
|
let skippedSkills = 0;
|
|
@@ -124,13 +122,27 @@ async function syncGlobalCache(claudeDir) {
|
|
|
124
122
|
skippedSkills++;
|
|
125
123
|
continue;
|
|
126
124
|
}
|
|
125
|
+
if (!skill.content)
|
|
126
|
+
continue; // skip skills with null/empty content
|
|
127
127
|
const skillFolder = path.join(skillsDir, skill.name);
|
|
128
128
|
fs.mkdirSync(skillFolder, { recursive: true });
|
|
129
129
|
const finalContent = injectAgentInfo(skill.content, skill.bot_ids);
|
|
130
|
-
fs.writeFileSync(path.join(skillFolder,
|
|
130
|
+
fs.writeFileSync(path.join(skillFolder, 'SKILL.md'), finalContent);
|
|
131
|
+
// Write reference files if present
|
|
132
|
+
if (skill.reference_files &&
|
|
133
|
+
typeof skill.reference_files === 'object' &&
|
|
134
|
+
Object.keys(skill.reference_files).length > 0) {
|
|
135
|
+
const refsDir = path.join(skillFolder, 'references');
|
|
136
|
+
fs.mkdirSync(refsDir, { recursive: true });
|
|
137
|
+
for (const [filename, refContent] of Object.entries(skill.reference_files)) {
|
|
138
|
+
if (typeof refContent === 'string') {
|
|
139
|
+
fs.writeFileSync(path.join(refsDir, filename), refContent);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
131
143
|
}
|
|
132
144
|
// 2. 커맨드 설치 (전체 교체)
|
|
133
|
-
const commandsDir = path.join(dir,
|
|
145
|
+
const commandsDir = path.join(dir, 'commands');
|
|
134
146
|
removeRecursive(commandsDir);
|
|
135
147
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
136
148
|
const commandsByFolder = {};
|
|
@@ -150,7 +162,7 @@ async function syncGlobalCache(claudeDir) {
|
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
164
|
// 3. 에이전트 설치 (전체 교체, 대소문자 중복 제거)
|
|
153
|
-
const agentsDir = path.join(dir,
|
|
165
|
+
const agentsDir = path.join(dir, 'agents');
|
|
154
166
|
removeRecursive(agentsDir);
|
|
155
167
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
156
168
|
const seenAgentNames = new Set();
|
|
@@ -170,8 +182,8 @@ async function syncGlobalCache(claudeDir) {
|
|
|
170
182
|
const agentDelegations = delegations.filter((d) => d.from_bot_id === agent.name);
|
|
171
183
|
if (agentDelegations.length > 0) {
|
|
172
184
|
const delegationLines = agentDelegations
|
|
173
|
-
.map((d) => `- → ${d.to_bot_id}: ${d.domains.join(
|
|
174
|
-
.join(
|
|
185
|
+
.map((d) => `- → ${d.to_bot_id}: ${d.domains.join(', ')} (via ${d.method})`)
|
|
186
|
+
.join('\n');
|
|
175
187
|
content += `\n\n## 위임 매트릭스\n${delegationLines}\n`;
|
|
176
188
|
}
|
|
177
189
|
// metadata에 model/description이 있으면 YAML frontmatter 주입
|