@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.
- package/dist/commands/bots.js +155 -2
- package/dist/kb.js +20 -5
- package/package.json +2 -1
package/dist/commands/bots.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
},
|