@team-semicolon/semo-cli 4.1.5 → 4.3.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/README.md +3 -4
- package/dist/commands/audit.d.ts +27 -0
- package/dist/commands/audit.js +338 -0
- package/dist/commands/bots.js +524 -24
- package/dist/commands/context.d.ts +14 -3
- package/dist/commands/context.js +192 -113
- package/dist/commands/db.d.ts +9 -0
- package/dist/commands/db.js +189 -0
- package/dist/commands/get.d.ts +1 -2
- package/dist/commands/get.js +24 -116
- package/dist/commands/memory.d.ts +8 -0
- package/dist/commands/memory.js +297 -0
- package/dist/commands/sessions.d.ts +2 -1
- package/dist/commands/sessions.js +31 -62
- package/dist/commands/skill-sync.d.ts +28 -0
- package/dist/commands/skill-sync.js +111 -0
- package/dist/commands/skill-sync.test.d.ts +16 -0
- package/dist/commands/skill-sync.test.js +186 -0
- package/dist/database.d.ts +42 -3
- package/dist/database.js +129 -554
- package/dist/env-parser.d.ts +5 -0
- package/dist/env-parser.js +27 -0
- package/dist/global-cache.d.ts +12 -0
- package/dist/global-cache.js +197 -0
- package/dist/index.js +515 -821
- package/dist/kb.d.ts +40 -39
- package/dist/kb.js +185 -176
- package/package.json +1 -1
package/dist/commands/get.js
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* semo get <resource> — 세션 중 실시간 DB 쿼리
|
|
4
4
|
*
|
|
5
|
-
* semo get projects [--active]
|
|
5
|
+
* semo get projects [--active] [--format table|json|md]
|
|
6
6
|
* semo get bots [--status online|offline]
|
|
7
7
|
* semo get kb [--domain <d>] [--key <k>] [--search <text>]
|
|
8
8
|
* semo get ontology [--domain <d>]
|
|
9
|
-
* semo get tasks [--project <p>] [--status <s>]
|
|
10
9
|
* semo get sessions [--bot <n>]
|
|
11
10
|
*/
|
|
12
11
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
@@ -52,69 +51,42 @@ function registerGetCommands(program) {
|
|
|
52
51
|
// ── semo get projects ───────────────────────────────────────
|
|
53
52
|
getCmd
|
|
54
53
|
.command("projects")
|
|
55
|
-
.description("프로젝트 목록 조회")
|
|
56
|
-
.option("--active", "활성 프로젝트만")
|
|
54
|
+
.description("프로젝트 목록 조회 (KB 기반)")
|
|
55
|
+
.option("--active", "활성 프로젝트만 (metadata.status='active')")
|
|
57
56
|
.option("--format <type>", "출력 형식 (table|json|md)", "table")
|
|
58
57
|
.action(async (options) => {
|
|
59
58
|
const spinner = (0, ora_1.default)("프로젝트 조회 중...").start();
|
|
60
59
|
const connected = await (0, database_1.isDbConnected)();
|
|
61
60
|
if (!connected) {
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const pool = (0, database_1.getPool)();
|
|
65
|
-
const { shared } = await (0, kb_1.kbList)(pool, { domain: "project", limit: 100 });
|
|
66
|
-
spinner.stop();
|
|
67
|
-
if (options.format === "json") {
|
|
68
|
-
console.log(JSON.stringify(shared, null, 2));
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
printTable(["key", "content"], shared.map(e => [e.key, e.content.substring(0, 80)]), "📁 프로젝트 (KB 기반)");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
spinner.fail("DB 연결 실패");
|
|
76
|
-
}
|
|
61
|
+
spinner.fail("DB 연결 실패");
|
|
77
62
|
await (0, database_1.closeConnection)();
|
|
78
|
-
|
|
63
|
+
process.exit(1);
|
|
79
64
|
}
|
|
80
65
|
try {
|
|
81
66
|
const pool = (0, database_1.getPool)();
|
|
82
|
-
|
|
83
|
-
let query = `
|
|
84
|
-
SELECT id, name, display_name, status, description, updated_at::text
|
|
85
|
-
FROM semo.projects
|
|
86
|
-
`;
|
|
87
|
-
const params = [];
|
|
67
|
+
let entries = await (0, kb_1.kbList)(pool, { domain: "project", limit: 100 });
|
|
88
68
|
if (options.active) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const result = await client.query(query, params);
|
|
95
|
-
rows = result.rows;
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
// semo.projects table may not exist — fallback to KB
|
|
99
|
-
client.release();
|
|
100
|
-
const { shared } = await (0, kb_1.kbList)(pool, { domain: "project", limit: 100 });
|
|
101
|
-
spinner.stop();
|
|
102
|
-
if (options.format === "json") {
|
|
103
|
-
console.log(JSON.stringify(shared, null, 2));
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
printTable(["key", "content"], shared.map(e => [e.key, e.content.substring(0, 80)]), "📁 프로젝트 (KB 기반)");
|
|
107
|
-
}
|
|
108
|
-
await (0, database_1.closeConnection)();
|
|
109
|
-
return;
|
|
69
|
+
entries = entries.filter(e => {
|
|
70
|
+
const meta = e.metadata;
|
|
71
|
+
return meta?.status === "active";
|
|
72
|
+
});
|
|
110
73
|
}
|
|
111
|
-
client.release();
|
|
112
74
|
spinner.stop();
|
|
113
75
|
if (options.format === "json") {
|
|
114
|
-
console.log(JSON.stringify(
|
|
76
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
77
|
+
}
|
|
78
|
+
else if (options.format === "md") {
|
|
79
|
+
for (const e of entries) {
|
|
80
|
+
console.log(`\n## ${e.key}\n`);
|
|
81
|
+
console.log(e.content);
|
|
82
|
+
}
|
|
115
83
|
}
|
|
116
84
|
else {
|
|
117
|
-
printTable(["
|
|
85
|
+
printTable(["key", "content", "updated_at"], entries.map(e => [
|
|
86
|
+
e.key,
|
|
87
|
+
(e.content || "").substring(0, 60),
|
|
88
|
+
e.updated_at ? new Date(e.updated_at).toLocaleString("ko-KR") : "-",
|
|
89
|
+
]), "📁 프로젝트 (KB)");
|
|
118
90
|
}
|
|
119
91
|
}
|
|
120
92
|
catch (err) {
|
|
@@ -217,11 +189,11 @@ function registerGetCommands(program) {
|
|
|
217
189
|
entries = result.rows;
|
|
218
190
|
}
|
|
219
191
|
else {
|
|
220
|
-
const
|
|
192
|
+
const kbEntries = await (0, kb_1.kbList)(pool, {
|
|
221
193
|
domain: options.domain,
|
|
222
194
|
limit,
|
|
223
195
|
});
|
|
224
|
-
entries =
|
|
196
|
+
entries = kbEntries;
|
|
225
197
|
}
|
|
226
198
|
spinner.stop();
|
|
227
199
|
if (options.format === "json") {
|
|
@@ -301,70 +273,6 @@ function registerGetCommands(program) {
|
|
|
301
273
|
await (0, database_1.closeConnection)();
|
|
302
274
|
}
|
|
303
275
|
});
|
|
304
|
-
// ── semo get tasks ──────────────────────────────────────────
|
|
305
|
-
getCmd
|
|
306
|
-
.command("tasks")
|
|
307
|
-
.description("태스크 조회 (semo.tasks)")
|
|
308
|
-
.option("--project <name>", "프로젝트 필터")
|
|
309
|
-
.option("--status <s>", "상태 필터 (open|in_progress|done)")
|
|
310
|
-
.option("--limit <n>", "최대 결과 수", "20")
|
|
311
|
-
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
312
|
-
.action(async (options) => {
|
|
313
|
-
const spinner = (0, ora_1.default)("태스크 조회 중...").start();
|
|
314
|
-
const connected = await (0, database_1.isDbConnected)();
|
|
315
|
-
if (!connected) {
|
|
316
|
-
spinner.fail("DB 연결 실패");
|
|
317
|
-
await (0, database_1.closeConnection)();
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
try {
|
|
321
|
-
const pool = (0, database_1.getPool)();
|
|
322
|
-
const client = await pool.connect();
|
|
323
|
-
let query = "SELECT id, title, status, project_id, assignee_name, updated_at::text FROM semo.tasks";
|
|
324
|
-
const params = [];
|
|
325
|
-
const conditions = [];
|
|
326
|
-
let idx = 1;
|
|
327
|
-
if (options.project) {
|
|
328
|
-
conditions.push(`project_id = $${idx++}`);
|
|
329
|
-
params.push(options.project);
|
|
330
|
-
}
|
|
331
|
-
if (options.status) {
|
|
332
|
-
conditions.push(`status = $${idx++}`);
|
|
333
|
-
params.push(options.status);
|
|
334
|
-
}
|
|
335
|
-
if (conditions.length > 0) {
|
|
336
|
-
query += " WHERE " + conditions.join(" AND ");
|
|
337
|
-
}
|
|
338
|
-
query += ` ORDER BY updated_at DESC LIMIT $${idx++}`;
|
|
339
|
-
params.push(parseInt(options.limit));
|
|
340
|
-
let rows = [];
|
|
341
|
-
try {
|
|
342
|
-
const result = await client.query(query, params);
|
|
343
|
-
rows = result.rows;
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
client.release();
|
|
347
|
-
spinner.warn("semo.tasks 테이블이 없거나 접근 불가");
|
|
348
|
-
await (0, database_1.closeConnection)();
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
client.release();
|
|
352
|
-
spinner.stop();
|
|
353
|
-
if (options.format === "json") {
|
|
354
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
357
|
-
printTable(["id", "title", "status", "assignee"], rows.map(r => [r.id, (r.title || "").substring(0, 50), r.status, r.assignee_name || "-"]), "📋 태스크");
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
catch (err) {
|
|
361
|
-
spinner.fail(`조회 실패: ${err}`);
|
|
362
|
-
process.exit(1);
|
|
363
|
-
}
|
|
364
|
-
finally {
|
|
365
|
-
await (0, database_1.closeConnection)();
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
276
|
// ── semo get sessions ───────────────────────────────────────
|
|
369
277
|
getCmd
|
|
370
278
|
.command("sessions")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semo memory sync — L1 (bot workspace) → L2 (KB) 메모리 동기화
|
|
3
|
+
*
|
|
4
|
+
* Bot workspace의 일일 메모리 파일(YYYY-MM-DD.md)을 KB memory 도메인으로 싱크.
|
|
5
|
+
* V1: LLM 요약 없이 raw 저장 (Garden 정책 확정 후 추가 예정)
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
export declare function registerMemoryCommands(program: Command): void;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* semo memory sync — L1 (bot workspace) → L2 (KB) 메모리 동기화
|
|
4
|
+
*
|
|
5
|
+
* Bot workspace의 일일 메모리 파일(YYYY-MM-DD.md)을 KB memory 도메인으로 싱크.
|
|
6
|
+
* V1: LLM 요약 없이 raw 저장 (Garden 정책 확정 후 추가 예정)
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.registerMemoryCommands = registerMemoryCommands;
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
const ora_1 = __importDefault(require("ora"));
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const crypto = __importStar(require("crypto"));
|
|
52
|
+
const database_1 = require("../database");
|
|
53
|
+
const kb_1 = require("../kb");
|
|
54
|
+
// ============================================================
|
|
55
|
+
// Constants
|
|
56
|
+
// ============================================================
|
|
57
|
+
const BOT_IDS = [
|
|
58
|
+
"semiclaw",
|
|
59
|
+
"workclaw",
|
|
60
|
+
"reviewclaw",
|
|
61
|
+
"planclaw",
|
|
62
|
+
"designclaw",
|
|
63
|
+
"infraclaw",
|
|
64
|
+
"growthclaw",
|
|
65
|
+
];
|
|
66
|
+
const MEMORY_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}\.md$/;
|
|
67
|
+
const STATE_DIR = path.join(os.homedir(), ".semo");
|
|
68
|
+
const STATE_FILE = path.join(STATE_DIR, "memory-sync-state.json");
|
|
69
|
+
// ============================================================
|
|
70
|
+
// State Management
|
|
71
|
+
// ============================================================
|
|
72
|
+
function readSyncState() {
|
|
73
|
+
try {
|
|
74
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
75
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8"));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// corrupted state file
|
|
80
|
+
}
|
|
81
|
+
return { synced: {} };
|
|
82
|
+
}
|
|
83
|
+
function writeSyncState(state) {
|
|
84
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
85
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
86
|
+
}
|
|
87
|
+
function contentHash(content) {
|
|
88
|
+
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
89
|
+
}
|
|
90
|
+
// ============================================================
|
|
91
|
+
// File Discovery
|
|
92
|
+
// ============================================================
|
|
93
|
+
function discoverBotMemoryFiles(botId, minAgeDays) {
|
|
94
|
+
const memoryDir = path.join(os.homedir(), `.openclaw-${botId}`, "workspace", "memory");
|
|
95
|
+
if (!fs.existsSync(memoryDir))
|
|
96
|
+
return [];
|
|
97
|
+
const candidates = [];
|
|
98
|
+
const cutoffDate = new Date();
|
|
99
|
+
cutoffDate.setDate(cutoffDate.getDate() - minAgeDays);
|
|
100
|
+
const files = fs.readdirSync(memoryDir);
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
if (!MEMORY_DATE_PATTERN.test(file))
|
|
103
|
+
continue;
|
|
104
|
+
const dateStr = file.replace(".md", "");
|
|
105
|
+
const fileDate = new Date(dateStr + "T23:59:59Z");
|
|
106
|
+
if (fileDate > cutoffDate)
|
|
107
|
+
continue; // Too recent
|
|
108
|
+
const filePath = path.join(memoryDir, file);
|
|
109
|
+
const content = fs.readFileSync(filePath, "utf-8").trim();
|
|
110
|
+
if (!content)
|
|
111
|
+
continue; // Skip empty files
|
|
112
|
+
candidates.push({
|
|
113
|
+
sourceType: "bot",
|
|
114
|
+
sourceId: botId,
|
|
115
|
+
date: dateStr,
|
|
116
|
+
filePath,
|
|
117
|
+
content,
|
|
118
|
+
hash: contentHash(content),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return candidates;
|
|
122
|
+
}
|
|
123
|
+
// ============================================================
|
|
124
|
+
// Sync Logic
|
|
125
|
+
// ============================================================
|
|
126
|
+
async function syncMemories(candidates, state, force, dryRun) {
|
|
127
|
+
let synced = 0;
|
|
128
|
+
let skipped = 0;
|
|
129
|
+
const errors = [];
|
|
130
|
+
if (candidates.length === 0) {
|
|
131
|
+
return { synced, skipped, errors };
|
|
132
|
+
}
|
|
133
|
+
const pool = (0, database_1.getPool)();
|
|
134
|
+
for (const candidate of candidates) {
|
|
135
|
+
const stateKey = `${candidate.sourceId}/${candidate.date}`;
|
|
136
|
+
// Check watermark
|
|
137
|
+
if (!force) {
|
|
138
|
+
const existing = state.synced[stateKey];
|
|
139
|
+
if (existing && existing.hash === candidate.hash) {
|
|
140
|
+
skipped++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (dryRun) {
|
|
145
|
+
console.log(chalk_1.default.gray(` [dry-run] ${stateKey} (${candidate.content.length} chars, hash: ${candidate.hash})`));
|
|
146
|
+
synced++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const domain = "memory";
|
|
151
|
+
const key = stateKey;
|
|
152
|
+
const metadata = {
|
|
153
|
+
source_type: candidate.sourceType,
|
|
154
|
+
source_id: candidate.sourceId,
|
|
155
|
+
date: candidate.date,
|
|
156
|
+
content_hash: candidate.hash,
|
|
157
|
+
original_size: candidate.content.length,
|
|
158
|
+
synced_at: new Date().toISOString(),
|
|
159
|
+
};
|
|
160
|
+
// Generate embedding
|
|
161
|
+
const text = `${key}: ${candidate.content}`;
|
|
162
|
+
const embedding = await (0, kb_1.generateEmbedding)(text);
|
|
163
|
+
const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
|
|
164
|
+
const client = await pool.connect();
|
|
165
|
+
try {
|
|
166
|
+
await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by, embedding)
|
|
167
|
+
VALUES ($1, $2, $3, $4, $5, $6::vector)
|
|
168
|
+
ON CONFLICT (domain, key) DO UPDATE SET
|
|
169
|
+
content = EXCLUDED.content,
|
|
170
|
+
metadata = EXCLUDED.metadata,
|
|
171
|
+
embedding = EXCLUDED.embedding`, [
|
|
172
|
+
domain,
|
|
173
|
+
key,
|
|
174
|
+
candidate.content,
|
|
175
|
+
JSON.stringify(metadata),
|
|
176
|
+
"semo-memory-sync",
|
|
177
|
+
embeddingStr,
|
|
178
|
+
]);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
client.release();
|
|
182
|
+
}
|
|
183
|
+
// Update watermark
|
|
184
|
+
state.synced[stateKey] = {
|
|
185
|
+
hash: candidate.hash,
|
|
186
|
+
syncedAt: new Date().toISOString(),
|
|
187
|
+
};
|
|
188
|
+
synced++;
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
errors.push(`${stateKey}: ${err}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { synced, skipped, errors };
|
|
195
|
+
}
|
|
196
|
+
// ============================================================
|
|
197
|
+
// Command Registration
|
|
198
|
+
// ============================================================
|
|
199
|
+
function registerMemoryCommands(program) {
|
|
200
|
+
const memoryCmd = program
|
|
201
|
+
.command("memory")
|
|
202
|
+
.description("메모리 관리 — L1(bot workspace) → L2(KB) 동기화");
|
|
203
|
+
memoryCmd
|
|
204
|
+
.command("sync")
|
|
205
|
+
.description("봇 워크스페이스 메모리를 KB memory 도메인으로 동기화")
|
|
206
|
+
.option("--source <type>", "소스 타입 (bot | local | all)", "all")
|
|
207
|
+
.option("--bot <id>", "특정 봇만 동기화")
|
|
208
|
+
.option("--days <n>", "N일 이상 경과한 메모리만 동기화", "2")
|
|
209
|
+
.option("--dry-run", "프리뷰만 (실제 동기화 안 함)")
|
|
210
|
+
.option("--force", "워터마크 무시, 전체 재동기화")
|
|
211
|
+
.action(async (options) => {
|
|
212
|
+
const dryRun = !!options.dryRun;
|
|
213
|
+
const force = !!options.force;
|
|
214
|
+
const minAgeDays = parseInt(options.days) || 2;
|
|
215
|
+
const sourceType = options.source;
|
|
216
|
+
const specificBot = options.bot;
|
|
217
|
+
const spinner = (0, ora_1.default)(dryRun ? "동기화 대상 탐색 중..." : "메모리 동기화 중...").start();
|
|
218
|
+
try {
|
|
219
|
+
const state = readSyncState();
|
|
220
|
+
let allCandidates = [];
|
|
221
|
+
// Discover bot memory files
|
|
222
|
+
if (sourceType === "bot" || sourceType === "all") {
|
|
223
|
+
const bots = specificBot ? [specificBot] : BOT_IDS;
|
|
224
|
+
for (const botId of bots) {
|
|
225
|
+
const candidates = discoverBotMemoryFiles(botId, minAgeDays);
|
|
226
|
+
allCandidates.push(...candidates);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
spinner.text = `${allCandidates.length}개 메모리 파일 발견`;
|
|
230
|
+
if (allCandidates.length === 0) {
|
|
231
|
+
spinner.succeed("동기화 대상 메모리 파일 없음");
|
|
232
|
+
await (0, database_1.closeConnection)();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const result = await syncMemories(allCandidates, state, force, dryRun);
|
|
236
|
+
if (!dryRun) {
|
|
237
|
+
writeSyncState(state);
|
|
238
|
+
}
|
|
239
|
+
if (dryRun) {
|
|
240
|
+
spinner.succeed(`[dry-run] ${result.synced}건 동기화 예정, ${result.skipped}건 스킵`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
spinner.succeed(`${result.synced}건 동기화 완료, ${result.skipped}건 스킵 (변경 없음)`);
|
|
244
|
+
}
|
|
245
|
+
if (result.errors.length > 0) {
|
|
246
|
+
console.log(chalk_1.default.yellow(` ⚠️ ${result.errors.length}건 오류:`));
|
|
247
|
+
for (const err of result.errors) {
|
|
248
|
+
console.log(chalk_1.default.red(` ${err}`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
console.log();
|
|
252
|
+
await (0, database_1.closeConnection)();
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
spinner.fail(`메모리 동기화 실패: ${err}`);
|
|
256
|
+
await (0, database_1.closeConnection)();
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
memoryCmd
|
|
261
|
+
.command("status")
|
|
262
|
+
.description("메모리 동기화 상태 확인")
|
|
263
|
+
.option("--bot <id>", "특정 봇만")
|
|
264
|
+
.action(async (options) => {
|
|
265
|
+
const state = readSyncState();
|
|
266
|
+
const specificBot = options.bot;
|
|
267
|
+
console.log(chalk_1.default.cyan.bold("\n📝 메모리 동기화 상태\n"));
|
|
268
|
+
const entries = Object.entries(state.synced);
|
|
269
|
+
if (entries.length === 0) {
|
|
270
|
+
console.log(chalk_1.default.yellow(" 동기화된 메모리 없음"));
|
|
271
|
+
console.log();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Group by source
|
|
275
|
+
const grouped = {};
|
|
276
|
+
for (const [key, val] of entries) {
|
|
277
|
+
const [sourceId, date] = key.split("/");
|
|
278
|
+
if (specificBot && sourceId !== specificBot)
|
|
279
|
+
continue;
|
|
280
|
+
if (!grouped[sourceId])
|
|
281
|
+
grouped[sourceId] = [];
|
|
282
|
+
grouped[sourceId].push({ date, ...val });
|
|
283
|
+
}
|
|
284
|
+
for (const [sourceId, items] of Object.entries(grouped)) {
|
|
285
|
+
console.log(chalk_1.default.white(` ${sourceId}:`));
|
|
286
|
+
const sorted = items.sort((a, b) => b.date.localeCompare(a.date));
|
|
287
|
+
const shown = sorted.slice(0, 10);
|
|
288
|
+
for (const item of shown) {
|
|
289
|
+
console.log(chalk_1.default.gray(` ${item.date} (synced: ${item.syncedAt.split("T")[0]})`));
|
|
290
|
+
}
|
|
291
|
+
if (sorted.length > 10) {
|
|
292
|
+
console.log(chalk_1.default.gray(` ... +${sorted.length - 10} more`));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
console.log();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
* { "agent:main:slack:channel:xxx": { updatedAt: <unix ms>, ... }, ... }
|
|
24
24
|
*/
|
|
25
25
|
import { Command } from "commander";
|
|
26
|
-
|
|
26
|
+
import { PoolClient } from "pg";
|
|
27
|
+
export declare function syncBotSessions(botIds: string[], client: PoolClient): Promise<{
|
|
27
28
|
total: number;
|
|
28
29
|
}>;
|
|
29
30
|
export declare function registerSessionsCommands(program: Command): void;
|