@team-semicolon/semo-cli 4.13.0 → 4.15.1

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/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 = {};
@@ -149,9 +161,8 @@ async function syncGlobalCache(claudeDir) {
149
161
  cmdCount++;
150
162
  }
151
163
  }
152
- // 3. 에이전트 설치 (전체 교체, 대소문자 중복 제거)
153
- const agentsDir = path.join(dir, "agents");
154
- removeRecursive(agentsDir);
164
+ // 3. 에이전트 설치 (머지 모드 로컬 YAML frontmatter 보존)
165
+ const agentsDir = path.join(dir, 'agents');
155
166
  fs.mkdirSync(agentsDir, { recursive: true });
156
167
  const seenAgentNames = new Set();
157
168
  const dedupedAgents = [];
@@ -162,30 +173,62 @@ async function syncGlobalCache(claudeDir) {
162
173
  dedupedAgents.push(agent);
163
174
  }
164
175
  }
176
+ // 기존 로컬 파일의 frontmatter 캐싱 (덮어쓰기 전)
177
+ const existingFrontmatters = new Map();
178
+ for (const folder of fs.readdirSync(agentsDir).filter((f) => !f.startsWith('.'))) {
179
+ const filePath = path.join(agentsDir, folder, `${folder}.md`);
180
+ if (fs.existsSync(filePath)) {
181
+ const existing = fs.readFileSync(filePath, 'utf8');
182
+ const fmMatch = existing.match(/^---\n([\s\S]*?)\n---\n/);
183
+ if (fmMatch) {
184
+ existingFrontmatters.set(folder.toLowerCase(), fmMatch[1]);
185
+ }
186
+ }
187
+ }
188
+ // DB에 없는 에이전트 폴더 정리 (orphan 제거)
189
+ const dbAgentNames = new Set(dedupedAgents.map((a) => a.name.toLowerCase()));
190
+ for (const folder of fs.readdirSync(agentsDir).filter((f) => !f.startsWith('.'))) {
191
+ const folderPath = path.join(agentsDir, folder);
192
+ if (!dbAgentNames.has(folder.toLowerCase()) && fs.statSync(folderPath).isDirectory()) {
193
+ removeRecursive(folderPath);
194
+ }
195
+ }
165
196
  for (const agent of dedupedAgents) {
166
197
  const agentFolder = path.join(agentsDir, agent.name);
167
198
  fs.mkdirSync(agentFolder, { recursive: true });
168
- let content = agent.content;
199
+ // DB content에서 frontmatter 제거 (body만 추출)
200
+ let dbBody = agent.content;
201
+ const dbFmMatch = agent.content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
202
+ if (dbFmMatch) {
203
+ dbBody = dbFmMatch[1].trim();
204
+ }
169
205
  // 위임 매트릭스 주입
170
206
  const agentDelegations = delegations.filter((d) => d.from_bot_id === agent.name);
171
207
  if (agentDelegations.length > 0) {
172
208
  const delegationLines = agentDelegations
173
- .map((d) => `- → ${d.to_bot_id}: ${d.domains.join(", ")} (via ${d.method})`)
174
- .join("\n");
175
- content += `\n\n## 위임 매트릭스\n${delegationLines}\n`;
209
+ .map((d) => `- → ${d.to_bot_id}: ${d.domains.join(', ')} (via ${d.method})`)
210
+ .join('\n');
211
+ dbBody += `\n\n## 위임 매트릭스\n${delegationLines}\n`;
176
212
  }
177
- // metadata에 model/description이 있으면 YAML frontmatter 주입
178
- if (agent.metadata && (agent.metadata.model || agent.metadata.description)) {
179
- const hasFrontmatter = content.trimStart().startsWith('---');
180
- if (!hasFrontmatter) {
181
- const fm = ['---'];
182
- if (agent.metadata.description)
183
- fm.push(`description: "${agent.metadata.description}"`);
184
- if (agent.metadata.model)
185
- fm.push(`model: "${agent.metadata.model}"`);
186
- fm.push('---', '');
187
- content = fm.join('\n') + content;
188
- }
213
+ // 로컬 frontmatter 보존 (있으면) DB body와 합침
214
+ const localFm = existingFrontmatters.get(agent.name.toLowerCase());
215
+ let content;
216
+ if (localFm) {
217
+ // 로컬 frontmatter 우선 보존 + DB body 업데이트
218
+ content = `---\n${localFm}\n---\n${dbBody}`;
219
+ }
220
+ else if (agent.metadata && (agent.metadata.model || agent.metadata.description)) {
221
+ // 로컬 frontmatter 없으면 metadata에서 생성
222
+ const fm = ['---'];
223
+ if (agent.metadata.description)
224
+ fm.push(`description: "${agent.metadata.description}"`);
225
+ if (agent.metadata.model)
226
+ fm.push(`model: "${agent.metadata.model}"`);
227
+ fm.push('---', '');
228
+ content = fm.join('\n') + dbBody;
229
+ }
230
+ else {
231
+ content = dbBody;
189
232
  }
190
233
  fs.writeFileSync(path.join(agentFolder, `${agent.name}.md`), content);
191
234
  }