@team-semicolon/semo-cli 4.4.3 → 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.
@@ -162,6 +162,101 @@ async function detectGatewayStatus(botId) {
162
162
  }
163
163
  }
164
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
+ // ============================================================
165
260
  // Command registration
166
261
  // ============================================================
167
262
  function registerBotsCommands(program) {
@@ -454,6 +549,29 @@ function registerBotsCommands(program) {
454
549
  catch {
455
550
  console.log(chalk_1.default.yellow(" ⚠ skills sync 실패 (무시)"));
456
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
+ }
457
575
  }
458
576
  catch (err) {
459
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.3",
3
+ "version": "4.6.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {