@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.
@@ -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(), "skill-sync-test-"));
60
- fs.mkdirSync(path.join(dir, "bot-workspaces"), { recursive: true });
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, "bot-workspaces", botId, "skills", skillName);
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, "SKILL.md"), content);
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("\n🧪 skill-sync.test.ts\n");
89
+ console.log('\n🧪 skill-sync.test.ts\n');
83
90
  // 1. 빈 디렉토리
84
- console.log("Case 1: 빈 디렉토리");
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, "skills = 0");
95
+ assert(skills.length === 0, 'skills = 0');
89
96
  }
90
97
  cleanup(tmpDir);
91
98
  // 2. 봇 전용 스킬 — flat name
92
- console.log("Case 2: 봇 전용 스킬 flat name");
99
+ console.log('Case 2: 봇 전용 스킬 flat name');
93
100
  tmpDir = setup();
94
101
  {
95
- makeBotSkill(tmpDir, "semiclaw", "github-issue-pipeline", "# GH Pipeline");
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 === "github-issue-pipeline", `name = github-issue-pipeline (got ${skills[0].name})`);
99
- assert(skills[0].botId === "semiclaw", "botId = semiclaw");
100
- assert(skills[0].package === "openclaw", "package = openclaw");
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("Case 3: 복수 봇 botId 정확성");
111
+ console.log('Case 3: 복수 봇 botId 정확성');
105
112
  tmpDir = setup();
106
113
  {
107
- makeBotSkill(tmpDir, "semiclaw", "skill-a", "# A");
108
- makeBotSkill(tmpDir, "semiclaw", "skill-b", "# B");
109
- makeBotSkill(tmpDir, "workclaw", "skill-c", "# C");
110
- makeBotSkill(tmpDir, "infraclaw", "skill-d", "# D");
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 === "semiclaw");
114
- const workclawSkills = skills.filter((s) => s.botId === "workclaw");
115
- const infraclawSkills = skills.filter((s) => s.botId === "infraclaw");
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("Case 4: .skill 확장자 디렉토리");
129
+ console.log('Case 4: .skill 확장자 디렉토리');
123
130
  tmpDir = setup();
124
131
  {
125
- const dotSkillDir = path.join(tmpDir, "bot-workspaces", "semiclaw", "skills", "old-format.skill");
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, "SKILL.md"), "should be skipped");
128
- makeBotSkill(tmpDir, "semiclaw", "valid-skill", "# Valid");
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 === "valid-skill", "올바른 스킬만 반환");
138
+ assert(skills[0].name === 'valid-skill', '올바른 스킬만 반환');
132
139
  }
133
140
  cleanup(tmpDir);
134
141
  // 5. SKILL.md 없는 디렉토리 → 스킵
135
- console.log("Case 5: SKILL.md 없는 디렉토리");
142
+ console.log('Case 5: SKILL.md 없는 디렉토리');
136
143
  tmpDir = setup();
137
144
  {
138
- const emptyDir = path.join(tmpDir, "bot-workspaces", "semiclaw", "skills", "no-skill-md");
145
+ const emptyDir = path.join(tmpDir, 'bot-workspaces', 'semiclaw', 'skills', 'no-skill-md');
139
146
  fs.mkdirSync(emptyDir, { recursive: true });
140
- makeBotSkill(tmpDir, "semiclaw", "valid-skill", "content");
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("Case 6: 스킬 변경 감지");
153
+ console.log('Case 6: 스킬 변경 감지');
147
154
  tmpDir = setup();
148
155
  {
149
- makeBotSkill(tmpDir, "semiclaw", "review", "# v1 content");
156
+ makeBotSkill(tmpDir, 'semiclaw', 'review', '# v1 content');
150
157
  const scan1 = (0, skill_sync_1.scanSkills)(tmpDir);
151
- assert(scan1[0].prompt === "# v1 content", "초기 스캔: v1");
152
- fs.writeFileSync(path.join(tmpDir, "bot-workspaces", "semiclaw", "skills", "review", "SKILL.md"), "# v2 updated content");
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 === "# v2 updated content", "재스캔: v2 반영됨");
161
+ assert(scan2[0].prompt === '# v2 updated content', '재스캔: v2 반영됨');
155
162
  }
156
163
  cleanup(tmpDir);
157
164
  // 7. 스킬 추가 감지 — 새 디렉토리 추가 후 재스캔
158
- console.log("Case 7: 스킬 추가 감지");
165
+ console.log('Case 7: 스킬 추가 감지');
159
166
  tmpDir = setup();
160
167
  {
161
- makeBotSkill(tmpDir, "semiclaw", "existing", "# Existing");
168
+ makeBotSkill(tmpDir, 'semiclaw', 'existing', '# Existing');
162
169
  const scan1 = (0, skill_sync_1.scanSkills)(tmpDir);
163
- assert(scan1.length === 1, "초기: 1개");
164
- makeBotSkill(tmpDir, "semiclaw", "new-skill", "# New Skill");
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("Case 8: 스킬 삭제 감지");
177
+ console.log('Case 8: 스킬 삭제 감지');
171
178
  tmpDir = setup();
172
179
  {
173
- makeBotSkill(tmpDir, "semiclaw", "to-delete", "# Will be deleted");
174
- makeBotSkill(tmpDir, "semiclaw", "keep", "# Keep");
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, "초기: 2개");
177
- fs.unlinkSync(path.join(tmpDir, "bot-workspaces", "semiclaw", "skills", "to-delete", "SKILL.md"));
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 === "keep", "남은 스킬: keep");
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${"".repeat(40)}`);
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);
@@ -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 "pg";
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(), ".claude", "semo", ".env");
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, "utf8"));
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("sslmode=require") ? { rejectUnauthorized: false } : false,
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("DB 연결 정보가 없습니다. DATABASE_URL 또는 SEMO_DB_HOST 환경변수를 설정하세요.");
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 || "5432"),
105
- user: process.env.SEMO_DB_USER || "app",
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 || "appdb",
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("SELECT 1");
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
- { id: "agent-semiclaw", name: "semiclaw", display_name: "SemiClaw 🦀", content: "", package: "openclaw", is_active: true, install_order: 1 },
144
- { id: "agent-workclaw", name: "workclaw", display_name: "WorkClaw 🛠️", content: "", package: "openclaw", is_active: true, install_order: 2 },
145
- { id: "agent-reviewclaw", name: "reviewclaw", display_name: "ReviewClaw 🔍", content: "", package: "openclaw", is_active: true, install_order: 3 },
146
- { id: "agent-planclaw", name: "planclaw", display_name: "PlanClaw 🗓️", content: "", package: "openclaw", is_active: true, install_order: 4 },
147
- { id: "agent-designclaw", name: "designclaw", display_name: "DesignClaw 🎨", content: "", package: "openclaw", is_active: true, install_order: 5 },
148
- { id: "agent-infraclaw", name: "infraclaw", display_name: "InfraClaw 🏗️", content: "", package: "openclaw", is_active: true, install_order: 6 },
149
- { id: "agent-growthclaw", name: "growthclaw", display_name: "GrowthClaw 🌱", content: "", package: "openclaw", is_active: true, install_order: 7 },
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
- { id: "pkg-cli", name: "semo-cli", display_name: "SEMO CLI", description: "CLI 도구 (컨텍스트 동기화, 봇 관리)", layer: "standard", package_type: "standard", version: "4.2.0", is_active: true, is_required: true, install_order: 10 },
153
- { id: "pkg-dashboard", name: "semo-dashboard", display_name: "SEMO Dashboard", description: "봇 상태/KB 대시보드", layer: "standard", package_type: "standard", version: "0.1.0", is_active: true, is_required: false, install_order: 30 },
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("⚠️ DB 연결 실패, 폴백 스킬 목록 사용");
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("⚠️ 스킬 조회 실패, 폴백 데이터 사용:", error);
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("⚠️ DB 연결 실패, 폴백 스킬 목록 사용");
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("⚠️ 봇 스킬 조회 실패, 폴백 데이터 사용:", error);
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("⚠️ DB 연결 실패, 폴백 커맨드 목록 사용");
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("⚠️ 커맨드 조회 실패, 폴백 데이터 사용:", error);
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("⚠️ DB 연결 실패, 폴백 에이전트 목록 사용");
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("⚠️ 에이전트 조회 실패, 폴백 데이터 사용:", error);
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("⚠️ DB 연결 실패, 폴백 패키지 목록 사용");
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("⚠️ 패키지 조회 실패, 폴백 데이터 사용:", error);
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
  }
@@ -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 === "win32";
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: "pipe" });
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: "pipe" });
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: "pipe" });
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(), ".claude");
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, "skills");
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, "SKILL.md"), finalContent);
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, "commands");
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, "agents");
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(", ")} (via ${d.method})`)
174
- .join("\n");
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 주입