@team-semicolon/semo-cli 4.3.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.js +10 -8
- 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.js +2 -2
- package/dist/index.js +333 -103
- package/dist/kb.d.ts +69 -0
- package/dist/kb.js +265 -16
- 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
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Declarative Workspace Audit Runner
|
|
4
|
+
*
|
|
5
|
+
* bot_workspace_standard 테이블에서 규칙을 로드하고,
|
|
6
|
+
* bot_status에서 봇 목록을 가져와 동적으로 TC를 생성·실행.
|
|
7
|
+
*
|
|
8
|
+
* 로컬 스크립트 의존성 없음 — DB가 SoT.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.runDeclarativeWorkspaceAudit = runDeclarativeWorkspaceAudit;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
// ============================================================
|
|
49
|
+
// Rule Checkers
|
|
50
|
+
// ============================================================
|
|
51
|
+
function checkExistence(fullPath) {
|
|
52
|
+
try {
|
|
53
|
+
const lstat = fs.lstatSync(fullPath);
|
|
54
|
+
return {
|
|
55
|
+
exists: true,
|
|
56
|
+
isSymlink: lstat.isSymbolicLink(),
|
|
57
|
+
isDir: lstat.isDirectory() || (lstat.isSymbolicLink() && fs.statSync(fullPath).isDirectory()),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { exists: false, isSymlink: false, isDir: false };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function checkGlob(wsPath, pattern) {
|
|
65
|
+
// Simple glob matching for common patterns
|
|
66
|
+
const matches = [];
|
|
67
|
+
if (pattern === "*/.git") {
|
|
68
|
+
// Check subdirectories for .git
|
|
69
|
+
try {
|
|
70
|
+
for (const entry of fs.readdirSync(wsPath)) {
|
|
71
|
+
const sub = path.join(wsPath, entry);
|
|
72
|
+
if (fs.statSync(sub).isDirectory() && entry !== ".git") {
|
|
73
|
+
if (fs.existsSync(path.join(sub, ".git"))) {
|
|
74
|
+
matches.push(entry);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch { /* */ }
|
|
80
|
+
}
|
|
81
|
+
else if (pattern === "node_modules") {
|
|
82
|
+
try {
|
|
83
|
+
const find = (dir, depth) => {
|
|
84
|
+
if (depth > 3)
|
|
85
|
+
return;
|
|
86
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
87
|
+
const full = path.join(dir, entry);
|
|
88
|
+
if (entry === "node_modules" && fs.statSync(full).isDirectory()) {
|
|
89
|
+
matches.push(path.relative(wsPath, full));
|
|
90
|
+
}
|
|
91
|
+
else if (fs.statSync(full).isDirectory() && !entry.startsWith(".")) {
|
|
92
|
+
find(full, depth + 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
find(wsPath, 0);
|
|
97
|
+
}
|
|
98
|
+
catch { /* */ }
|
|
99
|
+
}
|
|
100
|
+
else if (pattern.startsWith("*.")) {
|
|
101
|
+
// Glob for file extensions in root
|
|
102
|
+
const ext = pattern.slice(1); // ".ovpn", ".pem", etc.
|
|
103
|
+
try {
|
|
104
|
+
for (const entry of fs.readdirSync(wsPath)) {
|
|
105
|
+
if (entry.endsWith(ext) && fs.statSync(path.join(wsPath, entry)).isFile()) {
|
|
106
|
+
matches.push(entry);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch { /* */ }
|
|
111
|
+
}
|
|
112
|
+
return matches;
|
|
113
|
+
}
|
|
114
|
+
function checkSymlinkTarget(fullPath, expectedTarget) {
|
|
115
|
+
if (!expectedTarget)
|
|
116
|
+
return { ok: true, actual: null };
|
|
117
|
+
const resolved = expectedTarget.replace("$HOME", os.homedir());
|
|
118
|
+
try {
|
|
119
|
+
const actual = fs.readlinkSync(fullPath);
|
|
120
|
+
return { ok: actual === resolved, actual };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { ok: false, actual: null };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function checkContentRules(fullPath, rules) {
|
|
127
|
+
const details = [];
|
|
128
|
+
let allPassed = true;
|
|
129
|
+
try {
|
|
130
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
131
|
+
const lines = content.split("\n");
|
|
132
|
+
// max_lines
|
|
133
|
+
if (rules.max_lines !== undefined) {
|
|
134
|
+
if (lines.length > rules.max_lines) {
|
|
135
|
+
details.push(`줄 수 ${lines.length} > ${rules.max_lines}`);
|
|
136
|
+
allPassed = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// required_sections (grep for headings)
|
|
140
|
+
if (rules.required_sections) {
|
|
141
|
+
for (const section of rules.required_sections) {
|
|
142
|
+
const found = content.toLowerCase().includes(section.toLowerCase());
|
|
143
|
+
if (!found) {
|
|
144
|
+
details.push(`섹션 미발견: ${section}`);
|
|
145
|
+
allPassed = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// required_patterns (regex)
|
|
150
|
+
if (rules.required_patterns) {
|
|
151
|
+
for (const pat of rules.required_patterns) {
|
|
152
|
+
const regex = new RegExp(pat, "i");
|
|
153
|
+
if (!regex.test(content)) {
|
|
154
|
+
details.push(`패턴 미발견: ${pat}`);
|
|
155
|
+
allPassed = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// forbidden_patterns (should NOT match)
|
|
160
|
+
if (rules.forbidden_patterns) {
|
|
161
|
+
for (const pat of rules.forbidden_patterns) {
|
|
162
|
+
const regex = new RegExp(pat);
|
|
163
|
+
if (regex.test(content)) {
|
|
164
|
+
details.push(`금지 패턴 탐지: ${pat}`);
|
|
165
|
+
allPassed = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
details.push("파일 읽기 실패");
|
|
172
|
+
allPassed = false;
|
|
173
|
+
}
|
|
174
|
+
return { passed: allPassed, details };
|
|
175
|
+
}
|
|
176
|
+
// ============================================================
|
|
177
|
+
// Main Runner
|
|
178
|
+
// ============================================================
|
|
179
|
+
function checkRule(wsPath, botId, rule) {
|
|
180
|
+
const results = [];
|
|
181
|
+
const label = (msg) => `${botId}/${rule.path_pattern}: ${msg}`;
|
|
182
|
+
const caseId = `${botId}/${rule.path_pattern}`;
|
|
183
|
+
if (rule.entry_type === "glob") {
|
|
184
|
+
// Glob: check for forbidden matches
|
|
185
|
+
const matches = checkGlob(wsPath, rule.path_pattern);
|
|
186
|
+
if (rule.level === "forbidden") {
|
|
187
|
+
if (matches.length === 0) {
|
|
188
|
+
results.push({
|
|
189
|
+
type: "case",
|
|
190
|
+
id: caseId,
|
|
191
|
+
status: "pass",
|
|
192
|
+
label: label("없음"),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
for (const m of matches) {
|
|
197
|
+
results.push({
|
|
198
|
+
type: "case",
|
|
199
|
+
id: `${botId}/${m}`,
|
|
200
|
+
status: rule.severity === "error" ? "fail" : "warn",
|
|
201
|
+
label: label(`금지 항목 탐지: ${m}`),
|
|
202
|
+
detail: rule.description || undefined,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return results;
|
|
208
|
+
}
|
|
209
|
+
const fullPath = path.join(wsPath, rule.path_pattern);
|
|
210
|
+
const { exists, isSymlink, isDir } = checkExistence(fullPath);
|
|
211
|
+
// Required
|
|
212
|
+
if (rule.level === "required") {
|
|
213
|
+
if (!exists) {
|
|
214
|
+
results.push({
|
|
215
|
+
type: "case",
|
|
216
|
+
id: caseId,
|
|
217
|
+
status: "fail",
|
|
218
|
+
label: label("없음"),
|
|
219
|
+
detail: rule.description || undefined,
|
|
220
|
+
});
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
// Type check
|
|
224
|
+
if (rule.entry_type === "symlink" && !isSymlink) {
|
|
225
|
+
results.push({
|
|
226
|
+
type: "case",
|
|
227
|
+
id: caseId,
|
|
228
|
+
status: "fail",
|
|
229
|
+
label: label("심링크 아님"),
|
|
230
|
+
});
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
if (rule.entry_type === "dir" && !isDir) {
|
|
234
|
+
results.push({
|
|
235
|
+
type: "case",
|
|
236
|
+
id: caseId,
|
|
237
|
+
status: "fail",
|
|
238
|
+
label: label("디렉토리 아님"),
|
|
239
|
+
});
|
|
240
|
+
return results;
|
|
241
|
+
}
|
|
242
|
+
// Symlink target check
|
|
243
|
+
if (rule.entry_type === "symlink" && rule.symlink_target) {
|
|
244
|
+
const { ok, actual } = checkSymlinkTarget(fullPath, rule.symlink_target);
|
|
245
|
+
if (!ok) {
|
|
246
|
+
results.push({
|
|
247
|
+
type: "case",
|
|
248
|
+
id: caseId,
|
|
249
|
+
status: "fail",
|
|
250
|
+
label: label(`심링크 타겟 불일치 (actual: ${actual})`),
|
|
251
|
+
});
|
|
252
|
+
return results;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Content rules
|
|
256
|
+
if (rule.content_rules && rule.entry_type === "file") {
|
|
257
|
+
const { passed, details } = checkContentRules(fullPath, rule.content_rules);
|
|
258
|
+
if (!passed) {
|
|
259
|
+
results.push({
|
|
260
|
+
type: "case",
|
|
261
|
+
id: caseId,
|
|
262
|
+
status: rule.severity === "error" ? "fail" : "warn",
|
|
263
|
+
label: label(details.join("; ")),
|
|
264
|
+
});
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// All checks passed
|
|
269
|
+
results.push({
|
|
270
|
+
type: "case",
|
|
271
|
+
id: caseId,
|
|
272
|
+
status: "pass",
|
|
273
|
+
label: label("OK"),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// Optional — only warn if content rules fail
|
|
277
|
+
if (rule.level === "optional") {
|
|
278
|
+
if (!exists) {
|
|
279
|
+
results.push({
|
|
280
|
+
type: "case",
|
|
281
|
+
id: caseId,
|
|
282
|
+
status: "warn",
|
|
283
|
+
label: label("없음 (선택)"),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
results.push({
|
|
288
|
+
type: "case",
|
|
289
|
+
id: caseId,
|
|
290
|
+
status: "pass",
|
|
291
|
+
label: label("OK"),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Forbidden
|
|
296
|
+
if (rule.level === "forbidden") {
|
|
297
|
+
if (exists) {
|
|
298
|
+
results.push({
|
|
299
|
+
type: "case",
|
|
300
|
+
id: caseId,
|
|
301
|
+
status: rule.severity === "error" ? "fail" : "warn",
|
|
302
|
+
label: label("금지 항목 존재"),
|
|
303
|
+
detail: rule.description || undefined,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
results.push({
|
|
308
|
+
type: "case",
|
|
309
|
+
id: caseId,
|
|
310
|
+
status: "pass",
|
|
311
|
+
label: label("없음"),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return results;
|
|
316
|
+
}
|
|
317
|
+
async function runDeclarativeWorkspaceAudit(pool) {
|
|
318
|
+
// 1. Load rules from DB
|
|
319
|
+
const { rows: rules } = await pool.query(`SELECT path_pattern, entry_type, level, severity, category,
|
|
320
|
+
bot_scope, bot_ids, symlink_target, content_rules, description
|
|
321
|
+
FROM semo.bot_workspace_standard
|
|
322
|
+
WHERE spec_version = '2.0'
|
|
323
|
+
ORDER BY category, level DESC, path_pattern`);
|
|
324
|
+
// 2. Load bot list from DB
|
|
325
|
+
const { rows: bots } = await pool.query(`SELECT DISTINCT bot_id FROM semo.bot_status WHERE bot_id != 'shared' ORDER BY bot_id`);
|
|
326
|
+
const results = [];
|
|
327
|
+
// 3. Bot × Rule matrix
|
|
328
|
+
for (const bot of bots) {
|
|
329
|
+
const wsPath = path.join(os.homedir(), `.openclaw-${bot.bot_id}`, "workspace");
|
|
330
|
+
// Check workspace exists
|
|
331
|
+
if (!fs.existsSync(wsPath)) {
|
|
332
|
+
results.push({
|
|
333
|
+
type: "case",
|
|
334
|
+
id: `${bot.bot_id}/workspace`,
|
|
335
|
+
status: "fail",
|
|
336
|
+
label: `${bot.bot_id}: 워크스페이스 없음 (${wsPath})`,
|
|
337
|
+
});
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
for (const rule of rules) {
|
|
341
|
+
// bot_scope filter
|
|
342
|
+
if (rule.bot_scope === "include" &&
|
|
343
|
+
!rule.bot_ids.includes(bot.bot_id)) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (rule.bot_scope === "exclude" &&
|
|
347
|
+
rule.bot_ids.includes(bot.bot_id)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const ruleResults = checkRule(wsPath, bot.bot_id, rule);
|
|
351
|
+
results.push(...ruleResults);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// 4. Summary
|
|
355
|
+
let pass = 0, fail = 0, warn = 0;
|
|
356
|
+
for (const r of results) {
|
|
357
|
+
if (r.status === "pass")
|
|
358
|
+
pass++;
|
|
359
|
+
else if (r.status === "fail")
|
|
360
|
+
fail++;
|
|
361
|
+
else if (r.status === "warn")
|
|
362
|
+
warn++;
|
|
363
|
+
}
|
|
364
|
+
results.push({ type: "summary", pass, fail, warn });
|
|
365
|
+
return results;
|
|
366
|
+
}
|