@yemi33/minions 0.1.57 → 0.1.58
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/CHANGELOG.md +11 -0
- package/dashboard.js +41 -12
- package/engine.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard.js
CHANGED
|
@@ -635,6 +635,17 @@ function readBody(req) {
|
|
|
635
635
|
});
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
+
const _rateLimits = new Map();
|
|
639
|
+
function checkRateLimit(key, maxPerMinute) {
|
|
640
|
+
const now = Date.now();
|
|
641
|
+
const entries = _rateLimits.get(key) || [];
|
|
642
|
+
const recent = entries.filter(t => now - t < 60000);
|
|
643
|
+
if (recent.length >= maxPerMinute) return true;
|
|
644
|
+
recent.push(now);
|
|
645
|
+
_rateLimits.set(key, recent);
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
|
|
638
649
|
function jsonReply(res, code, data, req) {
|
|
639
650
|
res.setHeader('Content-Type', 'application/json');
|
|
640
651
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -1270,7 +1281,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1270
1281
|
'Content-Type': 'text/event-stream',
|
|
1271
1282
|
'Cache-Control': 'no-cache',
|
|
1272
1283
|
'Connection': 'keep-alive',
|
|
1273
|
-
'Access-Control-Allow-Origin': '*',
|
|
1274
1284
|
});
|
|
1275
1285
|
|
|
1276
1286
|
// Send initial content
|
|
@@ -1387,7 +1397,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1387
1397
|
const cat = match[1];
|
|
1388
1398
|
const file = decodeURIComponent(match[2]);
|
|
1389
1399
|
// Prevent path traversal
|
|
1390
|
-
if (file.includes('..') || file.includes('/') || file.includes('\\')) {
|
|
1400
|
+
if (file.includes('..') || file.includes('\0') || file.includes('/') || file.includes('\\')) {
|
|
1391
1401
|
return jsonReply(res, 400, { error: 'invalid file name' });
|
|
1392
1402
|
}
|
|
1393
1403
|
const content = safeRead(path.join(MINIONS_DIR, 'knowledge', cat, file));
|
|
@@ -1618,7 +1628,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1618
1628
|
|
|
1619
1629
|
async function handlePlansArchiveRead(req, res, match) {
|
|
1620
1630
|
const file = decodeURIComponent(match[1]);
|
|
1621
|
-
if (file.includes('..')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1631
|
+
if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1622
1632
|
// Check prd/archive/ first for .json, then plans/archive/ for .md
|
|
1623
1633
|
const archiveDir = file.endsWith('.json') ? path.join(PRD_DIR, 'archive') : path.join(PLANS_DIR, 'archive');
|
|
1624
1634
|
let content = safeRead(path.join(archiveDir, file));
|
|
@@ -1634,7 +1644,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1634
1644
|
|
|
1635
1645
|
async function handlePlansRead(req, res, match) {
|
|
1636
1646
|
const file = decodeURIComponent(match[1]);
|
|
1637
|
-
if (file.includes('..') || file.includes('/') || file.includes('\\')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1647
|
+
if (file.includes('..') || file.includes('\0') || file.includes('/') || file.includes('\\')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1638
1648
|
let content = safeRead(resolvePlanPath(file));
|
|
1639
1649
|
// Fallback: check all directories (prd/, plans/, guides/, archives)
|
|
1640
1650
|
if (!content) content = safeRead(path.join(PRD_DIR, file));
|
|
@@ -1801,7 +1811,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1801
1811
|
try {
|
|
1802
1812
|
const body = await readBody(req);
|
|
1803
1813
|
if (!body.file) return jsonReply(res, 400, { error: 'file is required' });
|
|
1804
|
-
if (body.file.includes('..')) return jsonReply(res, 400, { error: 'invalid file path' });
|
|
1814
|
+
if (body.file.includes('..') || body.file.includes('\0')) return jsonReply(res, 400, { error: 'invalid file path' });
|
|
1805
1815
|
|
|
1806
1816
|
const prdPath = path.join(PRD_DIR, body.file);
|
|
1807
1817
|
const plan = safeJson(prdPath);
|
|
@@ -1865,6 +1875,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1865
1875
|
}
|
|
1866
1876
|
|
|
1867
1877
|
async function handlePlansExecute(req, res) {
|
|
1878
|
+
if (checkRateLimit('plans-execute', 5)) return jsonReply(res, 429, { error: 'Rate limited — max 5 requests/minute' });
|
|
1868
1879
|
try {
|
|
1869
1880
|
const body = await readBody(req);
|
|
1870
1881
|
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
@@ -1975,7 +1986,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1975
1986
|
try {
|
|
1976
1987
|
const body = await readBody(req);
|
|
1977
1988
|
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
1978
|
-
if (body.file.includes('..') || body.file.includes('/') || body.file.includes('\\')) {
|
|
1989
|
+
if (body.file.includes('..') || body.file.includes('\0') || body.file.includes('/') || body.file.includes('\\')) {
|
|
1979
1990
|
return jsonReply(res, 400, { error: 'invalid filename' });
|
|
1980
1991
|
}
|
|
1981
1992
|
const planPath = resolvePlanPath(body.file);
|
|
@@ -2345,6 +2356,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2345
2356
|
const body = await readBody(req);
|
|
2346
2357
|
const { name } = body;
|
|
2347
2358
|
if (!name) return jsonReply(res, 400, { error: 'name required' });
|
|
2359
|
+
if (name.includes('..') || name.includes('\0')) return jsonReply(res, 400, { error: 'Invalid file name' });
|
|
2348
2360
|
|
|
2349
2361
|
const inboxPath = path.join(MINIONS_DIR, 'notes', 'inbox', name);
|
|
2350
2362
|
const content = safeRead(inboxPath);
|
|
@@ -2384,6 +2396,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2384
2396
|
const body = await readBody(req);
|
|
2385
2397
|
const { name, category } = body;
|
|
2386
2398
|
if (!name) return jsonReply(res, 400, { error: 'name required' });
|
|
2399
|
+
if (name.includes('..') || name.includes('\0')) return jsonReply(res, 400, { error: 'Invalid file name' });
|
|
2387
2400
|
if (!category || !shared.KB_CATEGORIES.includes(category)) {
|
|
2388
2401
|
return jsonReply(res, 400, { error: 'category required: ' + shared.KB_CATEGORIES.join(', ') });
|
|
2389
2402
|
}
|
|
@@ -2420,7 +2433,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2420
2433
|
try {
|
|
2421
2434
|
const body = await readBody(req);
|
|
2422
2435
|
const { name } = body;
|
|
2423
|
-
if (!name || name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
2436
|
+
if (!name || name.includes('..') || name.includes('\0') || name.includes('/') || name.includes('\\')) {
|
|
2424
2437
|
return jsonReply(res, 400, { error: 'invalid name' });
|
|
2425
2438
|
}
|
|
2426
2439
|
const filePath = path.join(MINIONS_DIR, 'notes', 'inbox', name);
|
|
@@ -2446,7 +2459,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2446
2459
|
try {
|
|
2447
2460
|
const body = await readBody(req);
|
|
2448
2461
|
const { name } = body;
|
|
2449
|
-
if (!name || name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
2462
|
+
if (!name || name.includes('..') || name.includes('\0') || name.includes('/') || name.includes('\\')) {
|
|
2450
2463
|
return jsonReply(res, 400, { error: 'invalid name' });
|
|
2451
2464
|
}
|
|
2452
2465
|
const filePath = path.join(MINIONS_DIR, 'notes', 'inbox', name);
|
|
@@ -2460,7 +2473,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2460
2473
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
|
2461
2474
|
const file = params.get('file');
|
|
2462
2475
|
const dir = params.get('dir');
|
|
2463
|
-
if (!file || file.includes('..')) { res.statusCode = 400; res.end('Invalid file'); return; }
|
|
2476
|
+
if (!file || file.includes('..') || file.includes('\0')) { res.statusCode = 400; res.end('Invalid file'); return; }
|
|
2464
2477
|
|
|
2465
2478
|
let content = '';
|
|
2466
2479
|
if (dir) {
|
|
@@ -2616,6 +2629,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2616
2629
|
}
|
|
2617
2630
|
|
|
2618
2631
|
async function handleCommandCenter(req, res) {
|
|
2632
|
+
if (checkRateLimit('command-center', 10)) return jsonReply(res, 429, { error: 'Rate limited — max 10 requests/minute' });
|
|
2619
2633
|
try {
|
|
2620
2634
|
const body = await readBody(req);
|
|
2621
2635
|
if (!body.message) return jsonReply(res, 400, { error: 'message required' });
|
|
@@ -3103,19 +3117,34 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3103
3117
|
// ── Route Dispatcher ────────────────────────────────────────────────────────
|
|
3104
3118
|
|
|
3105
3119
|
const pathname = req.url.split('?')[0];
|
|
3120
|
+
const _reqStart = Date.now();
|
|
3106
3121
|
for (const route of ROUTES) {
|
|
3107
3122
|
if (route.method !== req.method) continue;
|
|
3108
3123
|
if (typeof route.path === 'string') {
|
|
3109
3124
|
// For /api/skill, match with query string prefix since it has no fixed path variant
|
|
3110
3125
|
if (route.path === '/api/skill') {
|
|
3111
3126
|
if (!req.url.startsWith('/api/skill?') && req.url !== '/api/skill') continue;
|
|
3112
|
-
|
|
3127
|
+
const _result = await route.handler(req, res, {});
|
|
3128
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
3129
|
+
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
3130
|
+
}
|
|
3131
|
+
return _result;
|
|
3113
3132
|
}
|
|
3114
3133
|
if (pathname !== route.path) continue;
|
|
3115
|
-
|
|
3134
|
+
const _result = await route.handler(req, res, {});
|
|
3135
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
3136
|
+
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
3137
|
+
}
|
|
3138
|
+
return _result;
|
|
3116
3139
|
}
|
|
3117
3140
|
const m = pathname.match(route.path);
|
|
3118
|
-
if (m)
|
|
3141
|
+
if (m) {
|
|
3142
|
+
const _result = await route.handler(req, res, m);
|
|
3143
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
3144
|
+
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
3145
|
+
}
|
|
3146
|
+
return _result;
|
|
3147
|
+
}
|
|
3119
3148
|
}
|
|
3120
3149
|
|
|
3121
3150
|
// Serve dashboard HTML with gzip + caching
|
package/engine.js
CHANGED
|
@@ -102,7 +102,7 @@ function log(level, msg, meta = {}) {
|
|
|
102
102
|
let logData = safeJson(LOG_PATH) || [];
|
|
103
103
|
if (!Array.isArray(logData)) logData = logData.entries || [];
|
|
104
104
|
logData.push(entry);
|
|
105
|
-
if (logData.length >
|
|
105
|
+
if (logData.length > 2000) logData.splice(0, logData.length - 2000);
|
|
106
106
|
safeWrite(LOG_PATH, logData);
|
|
107
107
|
}
|
|
108
108
|
|
package/package.json
CHANGED