@team-semicolon/semo-cli 4.2.0 → 4.4.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.
@@ -51,11 +51,11 @@ function registerGetCommands(program) {
51
51
  // ── semo get projects ───────────────────────────────────────
52
52
  getCmd
53
53
  .command("projects")
54
- .description("프로젝트 목록 조회 (KB 기반)")
55
- .option("--active", "활성 프로젝트만 (metadata.status='active')")
54
+ .description("서비스 인스턴스 목록 조회 (온톨로지 기반)")
55
+ .option("--active", "활성 서비스만 (status='active')")
56
56
  .option("--format <type>", "출력 형식 (table|json|md)", "table")
57
57
  .action(async (options) => {
58
- const spinner = (0, ora_1.default)("프로젝트 조회 중...").start();
58
+ const spinner = (0, ora_1.default)("서비스 인스턴스 조회 중...").start();
59
59
  const connected = await (0, database_1.isDbConnected)();
60
60
  if (!connected) {
61
61
  spinner.fail("DB 연결 실패");
@@ -64,29 +64,43 @@ function registerGetCommands(program) {
64
64
  }
65
65
  try {
66
66
  const pool = (0, database_1.getPool)();
67
- let entries = await (0, kb_1.kbList)(pool, { domain: "project", limit: 100 });
67
+ const client = await pool.connect();
68
+ // Query service instances from ontology + their status from KB
69
+ const result = await client.query(`
70
+ SELECT o.domain, o.description,
71
+ ks.content as status,
72
+ (SELECT COUNT(*)::int FROM semo.knowledge_base k WHERE k.domain = o.domain) as entry_count,
73
+ (SELECT k2.content FROM semo.knowledge_base k2 WHERE k2.domain = o.domain AND k2.key = 'po' LIMIT 1) as po
74
+ FROM semo.ontology o
75
+ LEFT JOIN semo.knowledge_base ks ON ks.domain = o.domain AND ks.key = 'status'
76
+ WHERE o.entity_type = 'service'
77
+ ORDER BY o.domain
78
+ `);
79
+ client.release();
80
+ let rows = result.rows;
68
81
  if (options.active) {
69
- entries = entries.filter(e => {
70
- const meta = e.metadata;
71
- return meta?.status === "active";
72
- });
82
+ rows = rows.filter((r) => r.status === "active");
73
83
  }
74
84
  spinner.stop();
75
85
  if (options.format === "json") {
76
- console.log(JSON.stringify(entries, null, 2));
86
+ console.log(JSON.stringify(rows, null, 2));
77
87
  }
78
88
  else if (options.format === "md") {
79
- for (const e of entries) {
80
- console.log(`\n## ${e.key}\n`);
81
- console.log(e.content);
89
+ for (const r of rows) {
90
+ console.log(`\n## ${r.domain}\n`);
91
+ console.log(`상태: ${r.status || "-"} | 담당: ${r.po || "-"}`);
92
+ if (r.description)
93
+ console.log(r.description);
82
94
  }
83
95
  }
84
96
  else {
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)");
97
+ printTable(["service", "status", "po", "entries", "description"], rows.map((r) => [
98
+ r.domain,
99
+ r.status || "-",
100
+ r.po || "-",
101
+ String(r.entry_count || 0),
102
+ (r.description || "").substring(0, 40),
103
+ ]), "📁 서비스 인스턴스");
90
104
  }
91
105
  }
92
106
  catch (err) {
@@ -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,299 @@
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 flatKey = "memory";
152
+ const subKey = stateKey; // sourceId/date
153
+ const metadata = {
154
+ source_type: candidate.sourceType,
155
+ source_id: candidate.sourceId,
156
+ date: candidate.date,
157
+ content_hash: candidate.hash,
158
+ original_size: candidate.content.length,
159
+ synced_at: new Date().toISOString(),
160
+ };
161
+ // Generate embedding
162
+ const text = `memory/${stateKey}: ${candidate.content}`;
163
+ const embedding = await (0, kb_1.generateEmbedding)(text);
164
+ const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
165
+ const client = await pool.connect();
166
+ try {
167
+ await client.query(`INSERT INTO semo.knowledge_base (domain, key, sub_key, content, metadata, created_by, embedding)
168
+ VALUES ($1, $2, $3, $4, $5, $6, $7::vector)
169
+ ON CONFLICT (domain, key, sub_key) DO UPDATE SET
170
+ content = EXCLUDED.content,
171
+ metadata = EXCLUDED.metadata,
172
+ embedding = EXCLUDED.embedding`, [
173
+ domain,
174
+ flatKey,
175
+ subKey,
176
+ candidate.content,
177
+ JSON.stringify(metadata),
178
+ "semo-memory-sync",
179
+ embeddingStr,
180
+ ]);
181
+ }
182
+ finally {
183
+ client.release();
184
+ }
185
+ // Update watermark
186
+ state.synced[stateKey] = {
187
+ hash: candidate.hash,
188
+ syncedAt: new Date().toISOString(),
189
+ };
190
+ synced++;
191
+ }
192
+ catch (err) {
193
+ errors.push(`${stateKey}: ${err}`);
194
+ }
195
+ }
196
+ return { synced, skipped, errors };
197
+ }
198
+ // ============================================================
199
+ // Command Registration
200
+ // ============================================================
201
+ function registerMemoryCommands(program) {
202
+ const memoryCmd = program
203
+ .command("memory")
204
+ .description("메모리 관리 — L1(bot workspace) → L2(KB) 동기화");
205
+ memoryCmd
206
+ .command("sync")
207
+ .description("봇 워크스페이스 메모리를 KB memory 도메인으로 동기화")
208
+ .option("--source <type>", "소스 타입 (bot | local | all)", "all")
209
+ .option("--bot <id>", "특정 봇만 동기화")
210
+ .option("--days <n>", "N일 이상 경과한 메모리만 동기화", "2")
211
+ .option("--dry-run", "프리뷰만 (실제 동기화 안 함)")
212
+ .option("--force", "워터마크 무시, 전체 재동기화")
213
+ .action(async (options) => {
214
+ const dryRun = !!options.dryRun;
215
+ const force = !!options.force;
216
+ const minAgeDays = parseInt(options.days) || 2;
217
+ const sourceType = options.source;
218
+ const specificBot = options.bot;
219
+ const spinner = (0, ora_1.default)(dryRun ? "동기화 대상 탐색 중..." : "메모리 동기화 중...").start();
220
+ try {
221
+ const state = readSyncState();
222
+ let allCandidates = [];
223
+ // Discover bot memory files
224
+ if (sourceType === "bot" || sourceType === "all") {
225
+ const bots = specificBot ? [specificBot] : BOT_IDS;
226
+ for (const botId of bots) {
227
+ const candidates = discoverBotMemoryFiles(botId, minAgeDays);
228
+ allCandidates.push(...candidates);
229
+ }
230
+ }
231
+ spinner.text = `${allCandidates.length}개 메모리 파일 발견`;
232
+ if (allCandidates.length === 0) {
233
+ spinner.succeed("동기화 대상 메모리 파일 없음");
234
+ await (0, database_1.closeConnection)();
235
+ return;
236
+ }
237
+ const result = await syncMemories(allCandidates, state, force, dryRun);
238
+ if (!dryRun) {
239
+ writeSyncState(state);
240
+ }
241
+ if (dryRun) {
242
+ spinner.succeed(`[dry-run] ${result.synced}건 동기화 예정, ${result.skipped}건 스킵`);
243
+ }
244
+ else {
245
+ spinner.succeed(`${result.synced}건 동기화 완료, ${result.skipped}건 스킵 (변경 없음)`);
246
+ }
247
+ if (result.errors.length > 0) {
248
+ console.log(chalk_1.default.yellow(` ⚠️ ${result.errors.length}건 오류:`));
249
+ for (const err of result.errors) {
250
+ console.log(chalk_1.default.red(` ${err}`));
251
+ }
252
+ }
253
+ console.log();
254
+ await (0, database_1.closeConnection)();
255
+ }
256
+ catch (err) {
257
+ spinner.fail(`메모리 동기화 실패: ${err}`);
258
+ await (0, database_1.closeConnection)();
259
+ process.exit(1);
260
+ }
261
+ });
262
+ memoryCmd
263
+ .command("status")
264
+ .description("메모리 동기화 상태 확인")
265
+ .option("--bot <id>", "특정 봇만")
266
+ .action(async (options) => {
267
+ const state = readSyncState();
268
+ const specificBot = options.bot;
269
+ console.log(chalk_1.default.cyan.bold("\n📝 메모리 동기화 상태\n"));
270
+ const entries = Object.entries(state.synced);
271
+ if (entries.length === 0) {
272
+ console.log(chalk_1.default.yellow(" 동기화된 메모리 없음"));
273
+ console.log();
274
+ return;
275
+ }
276
+ // Group by source
277
+ const grouped = {};
278
+ for (const [stKey, val] of entries) {
279
+ const [sourceId, date] = stKey.split("/");
280
+ if (specificBot && sourceId !== specificBot)
281
+ continue;
282
+ if (!grouped[sourceId])
283
+ grouped[sourceId] = [];
284
+ grouped[sourceId].push({ date, ...val });
285
+ }
286
+ for (const [sourceId, items] of Object.entries(grouped)) {
287
+ console.log(chalk_1.default.white(` ${sourceId}:`));
288
+ const sorted = items.sort((a, b) => b.date.localeCompare(a.date));
289
+ const shown = sorted.slice(0, 10);
290
+ for (const item of shown) {
291
+ console.log(chalk_1.default.gray(` ${item.date} (synced: ${item.syncedAt.split("T")[0]})`));
292
+ }
293
+ if (sorted.length > 10) {
294
+ console.log(chalk_1.default.gray(` ... +${sorted.length - 10} more`));
295
+ }
296
+ }
297
+ console.log();
298
+ });
299
+ }
@@ -88,7 +88,7 @@ function scanSkills(semoSystemDir) {
88
88
  async function syncSkillsToDB(client, semoSystemDir) {
89
89
  const skills = scanSkills(semoSystemDir);
90
90
  for (const skill of skills) {
91
- await client.query(`INSERT INTO skill_definitions (name, prompt, package, metadata, is_active, office_id)
91
+ await client.query(`INSERT INTO semo.skill_definitions (name, prompt, package, metadata, is_active, office_id)
92
92
  VALUES ($1, $2, $3, $4, true, NULL)
93
93
  ON CONFLICT (name, office_id) DO UPDATE SET
94
94
  prompt = EXCLUDED.prompt,
@@ -0,0 +1,11 @@
1
+ /**
2
+ * semo test — 테스트 관리
3
+ *
4
+ * semo test list — 등록된 스위트 + 최근 실행 상태
5
+ * semo test run [suite] — 스위트 실행 + DB 기록
6
+ * semo test run --all — 전체 스위트 실행
7
+ * semo test run --notify — 실패 시 Slack 알림
8
+ * semo test history [suite] — 실행 이력
9
+ */
10
+ import { Command } from "commander";
11
+ export declare function registerTestCommands(program: Command): void;