@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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.58 (2026-03-30)
4
+
5
+ ### Engine
6
+ - engine.js
7
+
8
+ ### Dashboard
9
+ - dashboard.js
10
+
11
+ ### Other
12
+ - test/unit.test.js
13
+
3
14
  ## 0.1.57 (2026-03-30)
4
15
 
5
16
  ### Engine
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
- return await route.handler(req, res, {});
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
- return await route.handler(req, res, {});
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) return await route.handler(req, res, 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 > 500) logData.splice(0, logData.length - 500);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"