@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.
@@ -1,10 +1,15 @@
1
1
  /**
2
- * Bot Workspace Audit — 표준 구조 compliance 감사
2
+ * Bot Workspace Audit — v2.0 표준 구조 compliance 감사
3
+ *
4
+ * v2.0 규격 기준:
5
+ * - 필수 파일: SOUL.md, AGENTS.md (심링크), USER.md, MEMORY.md
6
+ * - 필수 디렉토리: .claude/, hooks/, memory/, skills/
7
+ * - 레거시 파일 부재 확인: IDENTITY.md, TOOLS.md, RULES.md
8
+ * - MEMORY.md slim check (< 30줄)
9
+ * - KB 도메인 존재 확인
3
10
  *
4
- * 12가지 체크를 수행하고 점수/등급을 산정한다.
5
- * - 파일 9개 (root 7 + memory/slim + skills/)
6
- * - KB 3개 (team, process, decision 도메인 존재)
7
11
  * --fix 옵션으로 누락 파일/디렉토리 자동 생성 가능.
12
+ * 레거시 파일은 --fix로 생성하지 않음 (v1→v2 전환 완료).
8
13
  */
9
14
  import { Pool, PoolClient } from "pg";
10
15
  export interface AuditCheck {
@@ -18,7 +23,10 @@ export interface BotAuditResult {
18
23
  score: number;
19
24
  checks: AuditCheck[];
20
25
  }
26
+ /** Sync version using fallback CHECK_DEFS (no DB) */
21
27
  export declare function auditBot(botDir: string, botId: string): BotAuditResult;
28
+ /** Async version using DB-loaded rules */
29
+ export declare function auditBotFromDb(botDir: string, botId: string, pool: Pool): Promise<BotAuditResult>;
22
30
  export declare function fixBot(botDir: string, botId: string, checks: AuditCheck[]): number;
23
31
  export declare function auditBotKb(pool: Pool): Promise<AuditCheck[]>;
24
32
  export declare function auditBotDb(botId: string, pool: Pool): Promise<AuditCheck[]>;
@@ -1,11 +1,16 @@
1
1
  "use strict";
2
2
  /**
3
- * Bot Workspace Audit — 표준 구조 compliance 감사
3
+ * Bot Workspace Audit — v2.0 표준 구조 compliance 감사
4
+ *
5
+ * v2.0 규격 기준:
6
+ * - 필수 파일: SOUL.md, AGENTS.md (심링크), USER.md, MEMORY.md
7
+ * - 필수 디렉토리: .claude/, hooks/, memory/, skills/
8
+ * - 레거시 파일 부재 확인: IDENTITY.md, TOOLS.md, RULES.md
9
+ * - MEMORY.md slim check (< 30줄)
10
+ * - KB 도메인 존재 확인
4
11
  *
5
- * 12가지 체크를 수행하고 점수/등급을 산정한다.
6
- * - 파일 9개 (root 7 + memory/slim + skills/)
7
- * - KB 3개 (team, process, decision 도메인 존재)
8
12
  * --fix 옵션으로 누락 파일/디렉토리 자동 생성 가능.
13
+ * 레거시 파일은 --fix로 생성하지 않음 (v1→v2 전환 완료).
9
14
  */
10
15
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
16
  if (k2 === undefined) k2 = k;
@@ -42,6 +47,7 @@ var __importStar = (this && this.__importStar) || (function () {
42
47
  })();
43
48
  Object.defineProperty(exports, "__esModule", { value: true });
44
49
  exports.auditBot = auditBot;
50
+ exports.auditBotFromDb = auditBotFromDb;
45
51
  exports.fixBot = fixBot;
46
52
  exports.auditBotKb = auditBotKb;
47
53
  exports.auditBotDb = auditBotDb;
@@ -65,6 +71,22 @@ function fileExistsCheck(name, relativePath) {
65
71
  },
66
72
  };
67
73
  }
74
+ function fileAbsentCheck(name, relativePath) {
75
+ return {
76
+ name,
77
+ check: (botDir) => {
78
+ const fullPath = path.join(botDir, relativePath);
79
+ const exists = fs.existsSync(fullPath);
80
+ return {
81
+ name,
82
+ passed: !exists,
83
+ detail: exists
84
+ ? `${relativePath} 레거시 파일 존재 (삭제 필요)`
85
+ : `${relativePath} 없음 (정상)`,
86
+ };
87
+ },
88
+ };
89
+ }
68
90
  function dirExistsCheck(name, relativePath) {
69
91
  return {
70
92
  name,
@@ -90,40 +112,188 @@ function memorySlimCheck() {
90
112
  return { name: "memory/slim", passed: true, detail: "MEMORY.md not present (N/A)" };
91
113
  }
92
114
  const lines = fs.readFileSync(memoryPath, "utf-8").split("\n").length;
93
- const passed = lines < 50;
115
+ const passed = lines <= 30;
94
116
  return {
95
117
  name: "memory/slim",
96
118
  passed,
97
119
  detail: passed
98
- ? `MEMORY.md is ${lines} lines (< 50)`
99
- : `MEMORY.md is ${lines} lines (>= 50, bloated)`,
120
+ ? `MEMORY.md is ${lines} lines ( 30)`
121
+ : `MEMORY.md is ${lines} lines (> 30, bloated)`,
122
+ };
123
+ },
124
+ };
125
+ }
126
+ function symlinkCheck(name, relativePath, expectedTarget) {
127
+ return {
128
+ name,
129
+ check: (botDir) => {
130
+ const fullPath = path.join(botDir, relativePath);
131
+ if (!fs.existsSync(fullPath)) {
132
+ return { name, passed: false, detail: `${relativePath} 없음` };
133
+ }
134
+ const stats = fs.lstatSync(fullPath);
135
+ if (!stats.isSymbolicLink()) {
136
+ return { name, passed: false, detail: `${relativePath} 심링크 아님 (일반 파일)` };
137
+ }
138
+ const target = fs.readlinkSync(fullPath);
139
+ const isCorrect = target === expectedTarget;
140
+ return {
141
+ name,
142
+ passed: isCorrect,
143
+ detail: isCorrect
144
+ ? `${relativePath} → ${expectedTarget} (정상)`
145
+ : `${relativePath} → ${target} (기대: ${expectedTarget})`,
100
146
  };
101
147
  },
102
148
  };
103
149
  }
150
+ const SHARED_AGENTS_PATH = path.join(process.env.HOME || "/Users/reus", ".openclaw-shared", "AGENTS.md");
104
151
  const CHECK_DEFS = [
105
- // 1-7: Root files
152
+ // v2.0 필수 파일
106
153
  fileExistsCheck("root/SOUL.md", "SOUL.md"),
107
- fileExistsCheck("root/IDENTITY.md", "IDENTITY.md"),
108
154
  fileExistsCheck("root/AGENTS.md", "AGENTS.md"),
155
+ symlinkCheck("root/AGENTS.md-symlink", "AGENTS.md", SHARED_AGENTS_PATH),
109
156
  fileExistsCheck("root/USER.md", "USER.md"),
110
- fileExistsCheck("root/TOOLS.md", "TOOLS.md"),
111
- fileExistsCheck("root/RULES.md", "RULES.md"),
112
157
  fileExistsCheck("root/MEMORY.md", "MEMORY.md"),
113
- // 8: Memory slim
114
- memorySlimCheck(),
115
- // 9: skills/
158
+ // v2.0 필수 디렉토리
159
+ dirExistsCheck(".claude/", ".claude"),
160
+ dirExistsCheck("hooks/", "hooks"),
161
+ dirExistsCheck("memory/", "memory"),
116
162
  dirExistsCheck("skills/", "skills"),
163
+ // Memory slim
164
+ memorySlimCheck(),
165
+ // v2.0 레거시 파일 부재 확인
166
+ fileAbsentCheck("legacy/IDENTITY.md", "IDENTITY.md"),
167
+ fileAbsentCheck("legacy/TOOLS.md", "TOOLS.md"),
168
+ fileAbsentCheck("legacy/RULES.md", "RULES.md"),
117
169
  ];
170
+ function buildChecksFromRow(row) {
171
+ const defs = [];
172
+ const name = `${row.category}/${row.path_pattern}`;
173
+ if (row.level === "required") {
174
+ if (row.entry_type === "symlink") {
175
+ const target = row.symlink_target?.replace("$HOME", process.env.HOME || "/Users/reus") || "";
176
+ defs.push(symlinkCheck(`${name}-symlink`, row.path_pattern, target));
177
+ defs.push(fileExistsCheck(name, row.path_pattern));
178
+ }
179
+ else if (row.entry_type === "dir") {
180
+ const dirPath = row.path_pattern.endsWith("/")
181
+ ? row.path_pattern.slice(0, -1)
182
+ : row.path_pattern;
183
+ defs.push(dirExistsCheck(name, dirPath));
184
+ }
185
+ else {
186
+ defs.push(fileExistsCheck(name, row.path_pattern));
187
+ }
188
+ }
189
+ else if (row.level === "forbidden") {
190
+ if (row.entry_type === "glob") {
191
+ // Glob forbidden checks require special handling — skip for basic audit
192
+ // (covered by hygiene checks in shell scripts)
193
+ }
194
+ else {
195
+ defs.push(fileAbsentCheck(name, row.path_pattern.replace(/\/$/, "")));
196
+ }
197
+ }
198
+ // Content rules: line count check
199
+ if (row.content_rules && typeof row.content_rules === "object") {
200
+ const rules = row.content_rules;
201
+ const maxLines = rules.max_lines;
202
+ if (maxLines && row.entry_type !== "dir") {
203
+ defs.push({
204
+ name: `${name}/lines`,
205
+ check: (botDir) => {
206
+ const fullPath = path.join(botDir, row.path_pattern);
207
+ if (!fs.existsSync(fullPath)) {
208
+ return { name: `${name}/lines`, passed: true, detail: `${row.path_pattern} not present (N/A)` };
209
+ }
210
+ const lines = fs.readFileSync(fullPath, "utf-8").split("\n").length;
211
+ const passed = lines <= maxLines;
212
+ return {
213
+ name: `${name}/lines`,
214
+ passed,
215
+ detail: passed
216
+ ? `${row.path_pattern} is ${lines} lines (≤ ${maxLines})`
217
+ : `${row.path_pattern} is ${lines} lines (> ${maxLines}, bloated)`,
218
+ };
219
+ },
220
+ });
221
+ }
222
+ }
223
+ return defs;
224
+ }
225
+ function botMatchesRow(row, botId) {
226
+ if (row.bot_scope === "all")
227
+ return true;
228
+ if (row.bot_scope === "include")
229
+ return row.bot_ids.includes(botId);
230
+ if (row.bot_scope === "exclude")
231
+ return !row.bot_ids.includes(botId);
232
+ return true;
233
+ }
234
+ async function loadCheckDefs(pool) {
235
+ const client = await pool.connect();
236
+ try {
237
+ const result = await client.query(`SELECT path_pattern, entry_type, level, severity, category,
238
+ bot_scope, bot_ids, symlink_target, content_rules,
239
+ description, fix_action, fix_template
240
+ FROM semo.bot_workspace_standard
241
+ WHERE spec_version = '2.0'
242
+ ORDER BY level, path_pattern`);
243
+ const rows = result.rows;
244
+ const defs = [];
245
+ for (const row of rows) {
246
+ defs.push(...buildChecksFromRow(row));
247
+ }
248
+ return { defs, rows };
249
+ }
250
+ finally {
251
+ client.release();
252
+ }
253
+ }
254
+ function buildCheckDefsForBot(allDefs, allRows, botId) {
255
+ // Filter defs based on bot_scope. Map row index to defs.
256
+ const filtered = [];
257
+ let defIdx = 0;
258
+ for (const row of allRows) {
259
+ const rowDefs = buildChecksFromRow(row);
260
+ if (botMatchesRow(row, botId)) {
261
+ filtered.push(...rowDefs);
262
+ }
263
+ defIdx += rowDefs.length;
264
+ }
265
+ return filtered;
266
+ }
118
267
  // ============================================================
119
268
  // Core audit function
120
269
  // ============================================================
270
+ /** Sync version using fallback CHECK_DEFS (no DB) */
121
271
  function auditBot(botDir, botId) {
122
272
  const checks = CHECK_DEFS.map((def) => def.check(botDir, botId));
273
+ return computeRating(botId, checks);
274
+ }
275
+ /** Async version using DB-loaded rules */
276
+ async function auditBotFromDb(botDir, botId, pool) {
277
+ try {
278
+ const { rows } = await loadCheckDefs(pool);
279
+ const defs = buildCheckDefsForBot([], rows, botId);
280
+ if (defs.length === 0) {
281
+ // Fallback if DB returns nothing
282
+ return auditBot(botDir, botId);
283
+ }
284
+ const checks = defs.map((def) => def.check(botDir, botId));
285
+ return computeRating(botId, checks);
286
+ }
287
+ catch {
288
+ // Offline fallback
289
+ return auditBot(botDir, botId);
290
+ }
291
+ }
292
+ function computeRating(botId, checks) {
123
293
  const passed = checks.filter((c) => c.passed).length;
124
294
  const total = checks.length;
125
- const score = Math.round((passed / total) * 100);
126
- const memorySlim = checks.find((c) => c.name === "memory/slim");
295
+ const score = total > 0 ? Math.round((passed / total) * 100) : 100;
296
+ const memorySlim = checks.find((c) => c.name.includes("/lines") && c.name.includes("MEMORY"));
127
297
  const isMemorySlim = memorySlim ? memorySlim.passed : true;
128
298
  let rating;
129
299
  if (score >= 80 && isMemorySlim) {
@@ -138,29 +308,28 @@ function auditBot(botDir, botId) {
138
308
  return { botId, rating, score, checks };
139
309
  }
140
310
  // ============================================================
141
- // Auto-fix
311
+ // Auto-fix (v2.0 — 레거시 파일은 생성하지 않음)
142
312
  // ============================================================
143
313
  const FIXABLE_FILES = {
144
314
  "root/SOUL.md": "SOUL.md",
145
- "root/IDENTITY.md": "IDENTITY.md",
146
- "root/AGENTS.md": "AGENTS.md",
147
315
  "root/USER.md": "USER.md",
148
- "root/TOOLS.md": "TOOLS.md",
149
- "root/RULES.md": "RULES.md",
150
316
  "root/MEMORY.md": "MEMORY.md",
151
317
  };
152
318
  const FIXABLE_DIRS = {
319
+ ".claude/": ".claude",
320
+ "hooks/": "hooks",
321
+ "memory/": "memory",
153
322
  "skills/": "skills",
154
323
  };
155
324
  const FILE_TEMPLATES = {
156
- "RULES.md": (botId) => `# RULES.md — ${botId} 행동 규칙\n\n> 공식 표준: kb_get(domain='process', key='bot-workspace-standard')\n\n## NON-NEGOTIABLE\n\n## 행동 원칙\n\n## 금지 사항\n`,
325
+ "SOUL.md": (botId) => `# ${botId} SOUL\n\n## Identity\n\n> TODO\n\n## R&R\n\n> TODO\n\n## KB Lookup Protocol\n\n> semo kb get/search로 팀 정보 조회\n\n## Operating Procedures\n\n> TODO\n\n## NON-NEGOTIABLE\n\n1. TODO\n`,
157
326
  };
158
327
  function fixBot(botDir, botId, checks) {
159
328
  let fixed = 0;
160
329
  for (const check of checks) {
161
330
  if (check.passed)
162
331
  continue;
163
- // Fix missing files
332
+ // Fix missing files (v2.0 허용 파일만)
164
333
  if (FIXABLE_FILES[check.name]) {
165
334
  const relPath = FIXABLE_FILES[check.name];
166
335
  const fullPath = path.join(botDir, relPath);
@@ -185,16 +354,39 @@ function fixBot(botDir, botId, checks) {
185
354
  fixed++;
186
355
  }
187
356
  }
357
+ // Fix AGENTS.md symlink
358
+ if (check.name === "root/AGENTS.md" || check.name === "root/AGENTS.md-symlink") {
359
+ const agentsPath = path.join(botDir, "AGENTS.md");
360
+ if (fs.existsSync(SHARED_AGENTS_PATH)) {
361
+ // 기존 일반 파일이면 삭제 후 심링크 생성
362
+ if (fs.existsSync(agentsPath)) {
363
+ const stats = fs.lstatSync(agentsPath);
364
+ if (!stats.isSymbolicLink()) {
365
+ fs.unlinkSync(agentsPath);
366
+ }
367
+ else {
368
+ const target = fs.readlinkSync(agentsPath);
369
+ if (target !== SHARED_AGENTS_PATH) {
370
+ fs.unlinkSync(agentsPath);
371
+ }
372
+ else {
373
+ continue; // 이미 정상 심링크
374
+ }
375
+ }
376
+ }
377
+ fs.symlinkSync(SHARED_AGENTS_PATH, agentsPath);
378
+ fixed++;
379
+ }
380
+ }
381
+ // 레거시 파일은 --fix로 삭제하지 않음 (수동 확인 필요)
382
+ // legacy/ 체크는 경고만 표시
188
383
  }
189
384
  return fixed;
190
385
  }
191
386
  // ============================================================
192
- // DB storage
193
- // ============================================================
194
- // ============================================================
195
387
  // KB domain checks (async, requires pool)
196
388
  // ============================================================
197
- const KB_REQUIRED_DOMAINS = ["team", "process", "decision"];
389
+ const KB_REQUIRED_DOMAINS = ["semicolon"];
198
390
  async function auditBotKb(pool) {
199
391
  const checks = [];
200
392
  try {
@@ -56,7 +56,7 @@ const audit_1 = require("./audit");
56
56
  const skill_sync_1 = require("./skill-sync");
57
57
  const context_1 = require("./context");
58
58
  function parseIdentityMd(content) {
59
- // P2-2: 대소문자 무시, **Key:**/Key: 양쪽 지원, 100자 초과 시 잘못된 파싱으로 간주
59
+ // v1 호환: IDENTITY.md 또는 SOUL.md에서 Name/Emoji/Role 파싱
60
60
  const nameMatch = content.match(/(?:\*\*)?Name:(?:\*\*)?\s*(.+)/i);
61
61
  const emojiMatch = content.match(/(?:\*\*)?Emoji:(?:\*\*)?\s*(\S+)/i);
62
62
  const roleMatch = content.match(/(?:\*\*)?(?:Creature|Role|직책):(?:\*\*)?\s*(.+)/i);
@@ -69,17 +69,28 @@ function parseIdentityMd(content) {
69
69
  role: role && role.length <= 100 ? role : null,
70
70
  };
71
71
  }
72
- function scanBotWorkspaces(semoSystemDir) {
73
- const workspacesDir = path.join(semoSystemDir, "bot-workspaces");
74
- if (!fs.existsSync(workspacesDir))
75
- return [];
72
+ function parseSoulIdentity(content, botId) {
73
+ // v2.0: SOUL.md ## Identity 섹션에서 이름/역할 추출
74
+ // 첫 줄 "# {name} — SOUL" 패턴 또는 ## Identity 이후 내용
75
+ const titleMatch = content.match(/^#\s+(.+?)(?:\s*[—–-]\s*SOUL)?$/m);
76
+ const name = titleMatch ? titleMatch[1].trim() : botId;
77
+ // ## R&R 또는 ## Identity 아래 첫 줄에서 역할 추출
78
+ const rrMatch = content.match(/##\s*R&R\s*\n+(?:>\s*)?(.+)/i);
79
+ const role = rrMatch ? rrMatch[1].trim().substring(0, 100) : null;
80
+ // 이모지: 제목이나 첫 줄에서 추출
81
+ const emojiMatch = content.match(/([\u{1F300}-\u{1FAD6}\u{2600}-\u{27BF}])/u);
82
+ const emoji = emojiMatch ? emojiMatch[1] : null;
83
+ return { name, emoji, role };
84
+ }
85
+ const KNOWN_BOTS = ["semiclaw", "workclaw", "reviewclaw", "planclaw", "designclaw", "infraclaw", "growthclaw"];
86
+ function scanBotWorkspaces(_semoSystemDir) {
87
+ const home = process.env.HOME || "/Users/reus";
76
88
  const bots = [];
77
- const entries = fs.readdirSync(workspacesDir, { withFileTypes: true });
78
- for (const entry of entries) {
79
- if (!entry.isDirectory())
89
+ for (const botId of KNOWN_BOTS) {
90
+ // v2.0 SoT: ~/.openclaw-{bot}/workspace/
91
+ const botDir = path.join(home, `.openclaw-${botId}`, "workspace");
92
+ if (!fs.existsSync(botDir))
80
93
  continue;
81
- const botId = entry.name;
82
- const botDir = path.join(workspacesDir, botId);
83
94
  // Most recent file mtime
84
95
  let lastActive = null;
85
96
  try {
@@ -89,10 +100,17 @@ function scanBotWorkspaces(semoSystemDir) {
89
100
  }
90
101
  }
91
102
  catch { /* skip */ }
92
- // Parse IDENTITY.md
103
+ // v2.0: SOUL.md에서 Identity 파싱 (IDENTITY.md fallback)
93
104
  let identity = { name: null, emoji: null, role: null };
105
+ const soulPath = path.join(botDir, "SOUL.md");
94
106
  const identityPath = path.join(botDir, "IDENTITY.md");
95
- if (fs.existsSync(identityPath)) {
107
+ if (fs.existsSync(soulPath)) {
108
+ try {
109
+ identity = parseSoulIdentity(fs.readFileSync(soulPath, "utf-8"), botId);
110
+ }
111
+ catch { /* skip */ }
112
+ }
113
+ else if (fs.existsSync(identityPath)) {
96
114
  try {
97
115
  identity = parseIdentityMd(fs.readFileSync(identityPath, "utf-8"));
98
116
  }
@@ -371,7 +389,7 @@ function registerBotsCommands(program) {
371
389
  // Audit piggyback — sync 후 자동 audit 실행
372
390
  try {
373
391
  console.log(chalk_1.default.gray(" → audit 실행 중..."));
374
- let auditResults = bots.map(b => (0, audit_1.auditBot)(b.workspacePath, b.botId));
392
+ let auditResults = await Promise.all(bots.map(b => (0, audit_1.auditBotFromDb)(b.workspacePath, b.botId, pool)));
375
393
  // KB 도메인 체크 merge (팀 레벨 — 한 번 조회 후 전체 적용)
376
394
  const kbChecks = await (0, audit_1.auditBotKb)(pool);
377
395
  auditResults = auditResults.map(r => (0, audit_1.mergeDbChecks)(r, kbChecks));
@@ -419,36 +437,37 @@ function registerBotsCommands(program) {
419
437
  .option("--format <type>", "출력 형식 (table|json|slack)", "table")
420
438
  .option("--fix", "누락 파일/디렉토리 자동 생성")
421
439
  .option("--no-db", "DB 저장 건너뛰기")
422
- .option("--semo-system <path>", "semo-system 경로 (기본: ./semo-system)")
423
440
  .action(async (options) => {
424
- const cwd = process.cwd();
425
- const semoSystemDir = options.semoSystem
426
- ? path.resolve(options.semoSystem)
427
- : path.join(cwd, "semo-system");
428
- const workspacesDir = path.join(semoSystemDir, "bot-workspaces");
429
- if (!fs.existsSync(workspacesDir)) {
430
- console.log(chalk_1.default.red(`\n❌ bot-workspaces 디렉토리를 찾을 수 없습니다: ${workspacesDir}`));
431
- process.exit(1);
441
+ const home = process.env.HOME || "/Users/reus";
442
+ const spinner = (0, ora_1.default)("bot-workspaces audit 중... (v2.0 SoT: ~/.openclaw-*/workspace/)").start();
443
+ // v2.0: SoT는 ~/.openclaw-{bot}/workspace/
444
+ const botEntries = [];
445
+ for (const botId of KNOWN_BOTS) {
446
+ const botDir = path.join(home, `.openclaw-${botId}`, "workspace");
447
+ if (fs.existsSync(botDir)) {
448
+ botEntries.push({ botId, botDir });
449
+ }
432
450
  }
433
- const spinner = (0, ora_1.default)("bot-workspaces audit 중...").start();
434
- // Scan bot directories
435
- const entries = fs.readdirSync(workspacesDir, { withFileTypes: true });
436
- const botDirs = entries.filter(e => e.isDirectory());
437
- if (botDirs.length === 0) {
451
+ if (botEntries.length === 0) {
438
452
  spinner.warn("봇 워크스페이스가 없습니다.");
439
453
  return;
440
454
  }
441
- // Run audit
442
- const results = botDirs.map(e => {
443
- const botDir = path.join(workspacesDir, e.name);
444
- return (0, audit_1.auditBot)(botDir, e.name);
445
- });
455
+ // Run audit — try DB-based rules first, fallback to hardcoded
456
+ let results;
457
+ const dbConnected = await (0, database_1.isDbConnected)();
458
+ if (dbConnected) {
459
+ const pool = (0, database_1.getPool)();
460
+ results = await Promise.all(botEntries.map(({ botId, botDir }) => (0, audit_1.auditBotFromDb)(botDir, botId, pool)));
461
+ }
462
+ else {
463
+ results = botEntries.map(({ botId, botDir }) => (0, audit_1.auditBot)(botDir, botId));
464
+ }
446
465
  spinner.stop();
447
466
  // --fix
448
467
  if (options.fix) {
449
468
  let totalFixed = 0;
450
469
  for (const r of results) {
451
- const botDir = path.join(workspacesDir, r.botId);
470
+ const botDir = path.join(home, `.openclaw-${r.botId}`, "workspace");
452
471
  const fixed = (0, audit_1.fixBot)(botDir, r.botId, r.checks);
453
472
  if (fixed > 0) {
454
473
  console.log(chalk_1.default.green(` ✔ ${r.botId}: ${fixed}개 파일/디렉토리 생성`));
@@ -459,7 +478,7 @@ function registerBotsCommands(program) {
459
478
  console.log(chalk_1.default.green(`\n총 ${totalFixed}개 수정`));
460
479
  // Re-audit after fix
461
480
  for (let i = 0; i < results.length; i++) {
462
- const botDir = path.join(workspacesDir, results[i].botId);
481
+ const botDir = path.join(home, `.openclaw-${results[i].botId}`, "workspace");
463
482
  results[i] = (0, audit_1.auditBot)(botDir, results[i].botId);
464
483
  }
465
484
  }
@@ -559,11 +578,15 @@ function registerBotsCommands(program) {
559
578
  if (!botEntry.isDirectory())
560
579
  continue;
561
580
  const botDir = path.join(workspacesDir, botEntry.name);
581
+ // v2.0: SOUL.md에서 Identity 파싱 (IDENTITY.md fallback)
582
+ const soulPath2 = path.join(botDir, "SOUL.md");
562
583
  const identityPath = path.join(botDir, "IDENTITY.md");
563
- if (!fs.existsSync(identityPath))
584
+ if (!fs.existsSync(soulPath2) && !fs.existsSync(identityPath))
564
585
  continue;
565
586
  try {
566
- const identity = parseIdentityMd(fs.readFileSync(identityPath, "utf-8"));
587
+ const identity = fs.existsSync(soulPath2)
588
+ ? parseSoulIdentity(fs.readFileSync(soulPath2, "utf-8"), botEntry.name)
589
+ : parseIdentityMd(fs.readFileSync(identityPath, "utf-8"));
567
590
  // persona_prompt = SOUL.md + \n\n---\n\n + AGENTS.md
568
591
  const parts = [];
569
592
  const soulPath = path.join(botDir, "SOUL.md");
@@ -620,7 +643,7 @@ function registerBotsCommands(program) {
620
643
  if (options.reset) {
621
644
  spinnerDb.text = "기존 데이터 삭제 중...";
622
645
  await client.query("DELETE FROM agent_definitions");
623
- await client.query("DELETE FROM skill_definitions WHERE office_id IS NULL");
646
+ await client.query("DELETE FROM semo.skill_definitions WHERE office_id IS NULL");
624
647
  }
625
648
  // ─── 스킬 시딩 (공통 모듈) ──────────────────────────
626
649
  spinnerDb.text = `스킬 ${botSkills.length}개 시딩 중...`;
@@ -2,9 +2,9 @@
2
2
  * semo context — 스킬/캐시/크론잡 동기화
3
3
  *
4
4
  * sync: DB → 글로벌 캐시 (skills/commands/agents) + 스킬 DB 동기화 + 크론잡
5
- * push: .claude/memory/<domain>.md → DB (deprecated — kb_upsert MCP 도구로 대체)
5
+ * push: .claude/memory/<domain>.md → DB (deprecated — semo kb upsert로 대체)
6
6
  *
7
- * [v4.2.0] KB→md 파일 생성 제거 — semo-kb MCP 서버로 통일
7
+ * [v4.2.0] KB→md 파일 생성 제거 — semo CLI kb 명령어로 통일
8
8
  */
9
9
  import { Command } from "commander";
10
10
  import { Pool } from "pg";
@@ -3,9 +3,9 @@
3
3
  * semo context — 스킬/캐시/크론잡 동기화
4
4
  *
5
5
  * sync: DB → 글로벌 캐시 (skills/commands/agents) + 스킬 DB 동기화 + 크론잡
6
- * push: .claude/memory/<domain>.md → DB (deprecated — kb_upsert MCP 도구로 대체)
6
+ * push: .claude/memory/<domain>.md → DB (deprecated — semo kb upsert로 대체)
7
7
  *
8
- * [v4.2.0] KB→md 파일 생성 제거 — semo-kb MCP 서버로 통일
8
+ * [v4.2.0] KB→md 파일 생성 제거 — semo CLI kb 명령어로 통일
9
9
  */
10
10
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
11
  if (k2 === undefined) k2 = k;
@@ -68,7 +68,9 @@ function resolveMemoryDir(outDir) {
68
68
  }
69
69
  return path.join(require("os").homedir(), MEMORY_DIR);
70
70
  }
71
+ /** @deprecated context push uses legacy flat domains — prefer semo kb upsert */
71
72
  const KB_DOMAIN_MAP = {
73
+ semicolon: "semicolon.md",
72
74
  team: "team.md",
73
75
  project: "projects.md",
74
76
  decision: "decisions.md",
@@ -179,18 +181,18 @@ function parseMarkdownSections(content, domain) {
179
181
  }
180
182
  return entries;
181
183
  }
182
- // [v4.2.0] digestToMarkdown 제거 — MCP kb_digest로 대체
184
+ // [v4.2.0] digestToMarkdown 제거 — semo CLI로 대체
183
185
  // ============================================================
184
186
  // Commands
185
187
  // ============================================================
186
188
  function registerContextCommands(program) {
187
189
  const ctxCmd = program
188
190
  .command("context")
189
- .description("스킬/캐시/크론잡 동기화 (KB는 semo-kb MCP 서버)");
191
+ .description("스킬/캐시/크론잡 동기화 (KB는 semo CLI)");
190
192
  // ── semo context sync ──────────────────────────────────────
191
193
  ctxCmd
192
194
  .command("sync")
193
- .description("스킬/에이전트/캐시 동기화 + 크론잡 (KB는 semo-kb MCP 서버 사용)")
195
+ .description("스킬/에이전트/캐시 동기화 + 크론잡 (KB는 semo CLI 사용)")
194
196
  .option("--no-skills", "스킬 파일 → DB 동기화 건너뜀")
195
197
  .option("--out-dir <path>", "캐시 파일 출력 경로 (기본: .claude/memory/)")
196
198
  .option("--no-global-cache", "글로벌 캐시(skills/commands/agents) 동기화 건너뜀")
@@ -205,9 +207,9 @@ function registerContextCommands(program) {
205
207
  const pool = (0, database_1.getPool)();
206
208
  const memDir = ensureMemoryDir(resolveMemoryDir(options.outDir));
207
209
  try {
208
- // [v4.2.0] KB→md 파일 생성 제거 — MCP kb_search/kb_list/kb_bot_status/kb_ontology로 대체
210
+ // [v4.2.0] KB→md 파일 생성 제거 — semo CLI kb 명령어로 대체
209
211
  // 기존 memory/*.md (team, projects, decisions, infra, process, bots, ontology) 파일은
210
- // semo-kb MCP 서버가 실시간 DB 조회로 대체합니다.
212
+ // semo CLI가 실시간 DB 조회로 대체합니다.
211
213
  // 1. 스킬 파일 → skill_definitions DB 동기화
212
214
  if (options.skills !== false) {
213
215
  const semoSystemDir = path.join(process.cwd(), "semo-system");
@@ -251,27 +253,6 @@ function registerContextCommands(program) {
251
253
  catch {
252
254
  // 크론잡 동기화 실패는 비치명적
253
255
  }
254
- // [v4.2.0] KB Digest 제거 — MCP kb_digest로 대체
255
- // 4. MCP 서버 자동 등록 (.claude/settings.json)
256
- try {
257
- const semoRoot = process.cwd();
258
- const settingsPath = path.join(memDir, "..", "settings.json");
259
- if (fs.existsSync(settingsPath)) {
260
- const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
261
- if (!settings.mcpServers?.["semo-kb"]) {
262
- settings.mcpServers = settings.mcpServers || {};
263
- settings.mcpServers["semo-kb"] = {
264
- command: "node",
265
- args: [path.join(semoRoot, "packages/mcp-kb/dist/index.js")],
266
- };
267
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
268
- console.log(chalk_1.default.green(" ✓ semo-kb MCP 서버 자동 등록"));
269
- }
270
- }
271
- }
272
- catch {
273
- // MCP 자동 등록 실패는 비치명적
274
- }
275
256
  spinner.succeed("context sync 완료 — 스킬/캐시/크론잡 동기화");
276
257
  console.log(chalk_1.default.gray(` 저장 위치: ${memDir}`));
277
258
  }
@@ -290,8 +271,8 @@ function registerContextCommands(program) {
290
271
  .option("--dry-run", "실제 push 없이 변경사항만 미리보기")
291
272
  .option("--out-dir <path>", "메모리 파일 경로 (기본: .claude/memory/). OpenClaw 봇 workspace 지원용")
292
273
  .action(async (options) => {
293
- console.log(chalk_1.default.yellow("⚠️ [deprecated] context push는 kb_upsert MCP 도구로 대체 예정입니다."));
294
- console.log(chalk_1.default.yellow(" 봇/세션에서는 semo-kb MCP 서버의 kb_upsert 도구를 직접 사용하세요.\n"));
274
+ console.log(chalk_1.default.yellow("⚠️ [deprecated] context push는 semo kb upsert로 대체 예정입니다."));
275
+ console.log(chalk_1.default.yellow(" 봇/세션에서는 semo kb upsert 명령어를 직접 사용하세요.\n"));
295
276
  const domains = options.domain.split(",").map((d) => d.trim()).filter(Boolean);
296
277
  const memDir = resolveMemoryDir(options.outDir);
297
278
  // 각 도메인별 엔트리 수집