@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.
- package/dist/commands/audit.d.ts +12 -4
- package/dist/commands/audit.js +219 -27
- package/dist/commands/bots.js +60 -37
- package/dist/commands/context.d.ts +2 -2
- package/dist/commands/context.js +11 -30
- package/dist/commands/get.js +31 -17
- package/dist/commands/memory.d.ts +8 -0
- package/dist/commands/memory.js +299 -0
- package/dist/commands/skill-sync.js +1 -1
- package/dist/commands/test.d.ts +11 -0
- package/dist/commands/test.js +520 -0
- package/dist/database.d.ts +1 -0
- package/dist/database.js +4 -3
- package/dist/global-cache.js +13 -0
- package/dist/index.js +434 -45
- package/dist/kb.d.ts +85 -0
- package/dist/kb.js +331 -19
- package/dist/slack-notify.d.ts +8 -0
- package/dist/slack-notify.js +45 -0
- package/dist/test-runners/workspace-audit.d.ts +17 -0
- package/dist/test-runners/workspace-audit.js +366 -0
- package/package.json +1 -1
package/dist/commands/audit.d.ts
CHANGED
|
@@ -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[]>;
|
package/dist/commands/audit.js
CHANGED
|
@@ -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
|
|
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 (
|
|
99
|
-
: `MEMORY.md is ${lines} lines (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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
|
-
"
|
|
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 = ["
|
|
389
|
+
const KB_REQUIRED_DOMAINS = ["semicolon"];
|
|
198
390
|
async function auditBotKb(pool) {
|
|
199
391
|
const checks = [];
|
|
200
392
|
try {
|
package/dist/commands/bots.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
//
|
|
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(
|
|
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.
|
|
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
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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 —
|
|
5
|
+
* push: .claude/memory/<domain>.md → DB (deprecated — semo kb upsert로 대체)
|
|
6
6
|
*
|
|
7
|
-
* [v4.2.0] KB→md 파일 생성 제거 — semo
|
|
7
|
+
* [v4.2.0] KB→md 파일 생성 제거 — semo CLI kb 명령어로 통일
|
|
8
8
|
*/
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
import { Pool } from "pg";
|
package/dist/commands/context.js
CHANGED
|
@@ -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 —
|
|
6
|
+
* push: .claude/memory/<domain>.md → DB (deprecated — semo kb upsert로 대체)
|
|
7
7
|
*
|
|
8
|
-
* [v4.2.0] KB→md 파일 생성 제거 — semo
|
|
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 제거 —
|
|
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
|
|
191
|
+
.description("스킬/캐시/크론잡 동기화 (KB는 semo CLI)");
|
|
190
192
|
// ── semo context sync ──────────────────────────────────────
|
|
191
193
|
ctxCmd
|
|
192
194
|
.command("sync")
|
|
193
|
-
.description("스킬/에이전트/캐시 동기화 + 크론잡 (KB는 semo
|
|
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 파일 생성 제거 —
|
|
210
|
+
// [v4.2.0] KB→md 파일 생성 제거 — semo CLI kb 명령어로 대체
|
|
209
211
|
// 기존 memory/*.md (team, projects, decisions, infra, process, bots, ontology) 파일은
|
|
210
|
-
// semo
|
|
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는
|
|
294
|
-
console.log(chalk_1.default.yellow(" 봇/세션에서는 semo
|
|
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
|
// 각 도메인별 엔트리 수집
|