@team-semicolon/semo-cli 4.4.2 → 4.6.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.
@@ -50,6 +50,7 @@ const chalk_1 = __importDefault(require("chalk"));
50
50
  const ora_1 = __importDefault(require("ora"));
51
51
  const fs = __importStar(require("fs"));
52
52
  const path = __importStar(require("path"));
53
+ const os = __importStar(require("os"));
53
54
  const database_1 = require("../database");
54
55
  const sessions_1 = require("./sessions");
55
56
  const audit_1 = require("./audit");
@@ -140,6 +141,122 @@ function getAllFileMtimes(dir, depth = 0) {
140
141
  return times;
141
142
  }
142
143
  // ============================================================
144
+ // Gateway status detection
145
+ // ============================================================
146
+ async function detectGatewayStatus(botId) {
147
+ const configPath = path.join(os.homedir(), `.openclaw-${botId}`, 'openclaw.json');
148
+ if (!fs.existsSync(configPath))
149
+ return 'offline';
150
+ try {
151
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
152
+ const port = config?.gateway?.port;
153
+ if (!port)
154
+ return 'offline';
155
+ const res = await fetch(`http://127.0.0.1:${port}/`, {
156
+ signal: AbortSignal.timeout(1000),
157
+ });
158
+ return res.ok ? 'online' : 'offline';
159
+ }
160
+ catch {
161
+ return 'offline';
162
+ }
163
+ }
164
+ // ============================================================
165
+ // Workspace files sync → bot_workspace_files
166
+ // ============================================================
167
+ const crypto = __importStar(require("crypto"));
168
+ const BINARY_EXTS = new Set([
169
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.bmp', '.svg',
170
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
171
+ '.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
172
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx',
173
+ '.exe', '.dll', '.so', '.dylib', '.bin',
174
+ '.mp3', '.mp4', '.wav', '.avi', '.mov',
175
+ '.db', '.sqlite', '.sqlite3',
176
+ ]);
177
+ const MAX_FILE_SIZE = 512 * 1024; // 512KB
178
+ async function syncWorkspaceFiles(client, botId, workspaceDir) {
179
+ const files = [];
180
+ function scan(dir, relBase, depth) {
181
+ if (depth > 4)
182
+ return;
183
+ let entries;
184
+ try {
185
+ entries = fs.readdirSync(dir, { withFileTypes: true });
186
+ }
187
+ catch {
188
+ return;
189
+ }
190
+ for (const entry of entries) {
191
+ if (entry.name.startsWith('.'))
192
+ continue;
193
+ const fullPath = path.join(dir, entry.name);
194
+ const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
195
+ // Skip symlinks
196
+ try {
197
+ if (fs.lstatSync(fullPath).isSymbolicLink())
198
+ continue;
199
+ }
200
+ catch {
201
+ continue;
202
+ }
203
+ if (entry.isDirectory()) {
204
+ scan(fullPath, relPath, depth + 1);
205
+ }
206
+ else if (entry.isFile()) {
207
+ const ext = path.extname(entry.name).toLowerCase();
208
+ if (BINARY_EXTS.has(ext))
209
+ continue;
210
+ let stat;
211
+ try {
212
+ stat = fs.statSync(fullPath);
213
+ }
214
+ catch {
215
+ continue;
216
+ }
217
+ if (stat.size > MAX_FILE_SIZE)
218
+ continue;
219
+ let content;
220
+ try {
221
+ content = fs.readFileSync(fullPath, 'utf-8');
222
+ }
223
+ catch {
224
+ continue;
225
+ }
226
+ // Skip files with NULL bytes (binary masquerading as text)
227
+ if (content.includes('\0'))
228
+ continue;
229
+ const hash = crypto.createHash('sha256').update(content).digest('hex');
230
+ files.push({ relPath, content, hash, size: stat.size });
231
+ }
232
+ }
233
+ }
234
+ scan(workspaceDir, '', 0);
235
+ let upserted = 0;
236
+ for (const f of files) {
237
+ const result = await client.query(`INSERT INTO semo.bot_workspace_files (bot_id, file_path, content, file_size, file_hash, synced_at)
238
+ VALUES ($1, $2, $3, $4, $5, NOW())
239
+ ON CONFLICT (bot_id, file_path) DO UPDATE SET
240
+ content = EXCLUDED.content,
241
+ file_size = EXCLUDED.file_size,
242
+ file_hash = EXCLUDED.file_hash,
243
+ synced_at = NOW()
244
+ WHERE semo.bot_workspace_files.file_hash IS DISTINCT FROM EXCLUDED.file_hash`, [botId, f.relPath, f.content, f.size, f.hash]);
245
+ if (result.rowCount && result.rowCount > 0) {
246
+ upserted++;
247
+ }
248
+ }
249
+ // Delete files in DB but not on disk (for this bot_id)
250
+ const dbFiles = await client.query(`SELECT file_path FROM semo.bot_workspace_files WHERE bot_id = $1`, [botId]);
251
+ const diskPaths = new Set(files.map(f => f.relPath));
252
+ for (const row of dbFiles.rows) {
253
+ if (!diskPaths.has(row.file_path)) {
254
+ await client.query(`DELETE FROM semo.bot_workspace_files WHERE bot_id = $1 AND file_path = $2`, [botId, row.file_path]);
255
+ }
256
+ }
257
+ return files.length;
258
+ }
259
+ // ============================================================
143
260
  // Command registration
144
261
  // ============================================================
145
262
  function registerBotsCommands(program) {
@@ -324,16 +441,28 @@ function registerBotsCommands(program) {
324
441
  const errors = [];
325
442
  try {
326
443
  await client.query("BEGIN");
444
+ // Detect gateway status for all bots in parallel
445
+ const statusMap = new Map();
446
+ const statusResults = await Promise.all(bots.map(async (bot) => ({
447
+ botId: bot.botId,
448
+ status: await detectGatewayStatus(bot.botId),
449
+ })));
450
+ for (const { botId, status } of statusResults) {
451
+ statusMap.set(botId, status);
452
+ }
453
+ const onlineCount = statusResults.filter(r => r.status === 'online').length;
454
+ spinner.text = `${bots.length}개 봇 DB 반영 중... (게이트웨이: ${onlineCount}개 online)`;
327
455
  for (const bot of bots) {
328
456
  try {
457
+ const detectedStatus = statusMap.get(bot.botId) || 'offline';
329
458
  await client.query(`INSERT INTO semo.bot_status
330
459
  (bot_id, name, emoji, role, status, last_active, workspace_path, synced_at)
331
- VALUES ($1, $2, $3, $4, 'offline', $5, $6, NOW())
460
+ VALUES ($1, $2, $3, $4, $7, $5, $6, NOW())
332
461
  ON CONFLICT (bot_id) DO UPDATE SET
333
462
  name = COALESCE(EXCLUDED.name, semo.bot_status.name),
334
463
  emoji = COALESCE(EXCLUDED.emoji, semo.bot_status.emoji),
335
464
  role = COALESCE(EXCLUDED.role, semo.bot_status.role),
336
- status = semo.bot_status.status,
465
+ status = EXCLUDED.status,
337
466
  last_active = CASE
338
467
  WHEN EXCLUDED.last_active IS NOT NULL
339
468
  AND (semo.bot_status.last_active IS NULL
@@ -349,6 +478,7 @@ function registerBotsCommands(program) {
349
478
  bot.role,
350
479
  bot.lastActive?.toISOString() || null,
351
480
  bot.workspacePath,
481
+ detectedStatus,
352
482
  ]);
353
483
  upserted++;
354
484
  }
@@ -419,6 +549,29 @@ function registerBotsCommands(program) {
419
549
  catch {
420
550
  console.log(chalk_1.default.yellow(" ⚠ skills sync 실패 (무시)"));
421
551
  }
552
+ // Files piggyback — 워크스페이스 파일 → bot_workspace_files 동기화
553
+ try {
554
+ console.log(chalk_1.default.gray(" → files sync 실행 중..."));
555
+ const filesClient = await pool.connect();
556
+ try {
557
+ let totalFiles = 0;
558
+ for (const bot of bots) {
559
+ totalFiles += await syncWorkspaceFiles(filesClient, bot.botId, bot.workspacePath);
560
+ }
561
+ // Shared files
562
+ const sharedDir = path.join(os.homedir(), '.openclaw-shared');
563
+ if (fs.existsSync(sharedDir)) {
564
+ totalFiles += await syncWorkspaceFiles(filesClient, '_shared', sharedDir);
565
+ }
566
+ console.log(chalk_1.default.green(` → files sync 완료: ${totalFiles}개 파일`));
567
+ }
568
+ finally {
569
+ filesClient.release();
570
+ }
571
+ }
572
+ catch (filesErr) {
573
+ console.log(chalk_1.default.yellow(` ⚠ files sync 실패 (무시): ${filesErr}`));
574
+ }
422
575
  }
423
576
  catch (err) {
424
577
  await client.query("ROLLBACK");
package/dist/kb.js CHANGED
@@ -395,8 +395,22 @@ async function kbSearch(pool, query, options) {
395
395
  }
396
396
  // Text search (fallback or hybrid supplement)
397
397
  // Split query into tokens and match ANY token via ILIKE (Korean-friendly)
398
+ // For Korean tokens of 4+ chars with no spaces, add 2-char sub-tokens
399
+ // e.g. "노조관리" → ["노조관리", "노조", "관리"]
398
400
  if (mode !== "semantic" || results.length === 0) {
399
- const tokens = query.split(/\s+/).filter(t => t.length >= 2);
401
+ const rawTokens = query.split(/\s+/).filter(t => t.length >= 2);
402
+ const tokens = [];
403
+ const KOREAN_RE = /[\uAC00-\uD7AF]/;
404
+ for (const t of rawTokens) {
405
+ tokens.push(t);
406
+ if (KOREAN_RE.test(t) && t.length >= 4) {
407
+ for (let i = 0; i + 2 <= t.length; i += 2) {
408
+ const sub = t.slice(i, i + 2);
409
+ if (!tokens.includes(sub))
410
+ tokens.push(sub);
411
+ }
412
+ }
413
+ }
400
414
  const textParams = [];
401
415
  let tIdx = 1;
402
416
  // Build per-token ILIKE conditions + count matching tokens for scoring
@@ -700,7 +714,7 @@ async function kbUpsert(pool, entry) {
700
714
  client.release();
701
715
  }
702
716
  // Key validation against type schema
703
- try {
717
+ {
704
718
  const schemaClient = await pool.connect();
705
719
  try {
706
720
  const typeResult = await schemaClient.query("SELECT entity_type FROM semo.ontology WHERE domain = $1 AND entity_type IS NOT NULL", [entry.domain]);
@@ -726,13 +740,14 @@ async function kbUpsert(pool, entry) {
726
740
  }
727
741
  }
728
742
  }
743
+ catch (e) {
744
+ // DB 연결 실패 시 warning 로그 — 검증 자체는 스킵하되 사용자에게 알림
745
+ console.error(`[kb] ⚠️ 스키마 검증 DB 오류 (검증 건너뜀): ${e}`);
746
+ }
729
747
  finally {
730
748
  schemaClient.release();
731
749
  }
732
750
  }
733
- catch {
734
- // Validation failure is non-fatal
735
- }
736
751
  // Generate embedding (mandatory)
737
752
  const fullKey = combineKey(key, subKey);
738
753
  const text = `${fullKey}: ${entry.content}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "4.4.2",
3
+ "version": "4.6.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "build": "tsc",
12
+ "postbuild": "xattr -cr dist/ 2>/dev/null || true",
12
13
  "start": "node dist/index.js",
13
14
  "dev": "ts-node src/index.ts"
14
15
  },