@yemi33/minions 0.1.57 → 0.1.59

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/dashboard.js CHANGED
@@ -124,11 +124,11 @@ if (fs.existsSync(dashDir)) {
124
124
  _reloadTimer = setTimeout(rebuildDashboardHtml, 300); // debounce 300ms
125
125
  };
126
126
  // Watch top-level files (styles.css, layout.html)
127
- try { fs.watch(dashDir, scheduleReload); } catch {}
127
+ try { fs.watch(dashDir, scheduleReload); } catch { /* optional */ }
128
128
  // Watch subdirectories (pages/, js/)
129
129
  for (const sub of ['pages', 'js']) {
130
130
  const subDir = path.join(dashDir, sub);
131
- if (fs.existsSync(subDir)) try { fs.watch(subDir, scheduleReload); } catch {}
131
+ if (fs.existsSync(subDir)) try { fs.watch(subDir, scheduleReload); } catch { /* optional */ }
132
132
  }
133
133
  }
134
134
 
@@ -145,7 +145,7 @@ function getVerifyGuides() {
145
145
  const planFile = planSlug + '.json';
146
146
  guides.push({ file: f, planFile });
147
147
  }
148
- } catch {}
148
+ } catch (e) { console.error('getVerifyGuides:', e.message); }
149
149
  return guides;
150
150
  }
151
151
 
@@ -273,7 +273,7 @@ try {
273
273
  const age = Date.now() - new Date(saved.lastActiveAt || 0).getTime();
274
274
  if (age < CC_SESSION_EXPIRY_MS) ccSession = saved;
275
275
  }
276
- } catch {}
276
+ } catch { /* optional */ }
277
277
 
278
278
  // Static system prompt — baked into session on creation, never changes
279
279
  const CC_STATIC_SYSTEM_PROMPT = `You are the Command Center AI for a software engineering minions called "Minions."
@@ -459,7 +459,7 @@ try {
459
459
  }
460
460
  }
461
461
  }
462
- } catch {}
462
+ } catch { /* optional */ }
463
463
 
464
464
  function persistDocSessions() {
465
465
  const obj = {};
@@ -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', '*');
@@ -676,12 +687,12 @@ function cleanDispatchEntries(matchFn) {
676
687
  try {
677
688
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
678
689
  if (pid) process.kill(pid, 'SIGTERM');
679
- } catch {}
680
- try { fs.unlinkSync(pidFile); } catch {}
690
+ } catch { /* process may be dead */ }
691
+ try { fs.unlinkSync(pidFile); } catch { /* cleanup */ }
681
692
  // Clean up temp prompt files
682
- try { fs.unlinkSync(path.join(engineDir, 'tmp', `prompt-${d.id}.md`)); } catch {}
683
- try { fs.unlinkSync(path.join(engineDir, 'tmp', `sysprompt-${d.id}.md`)); } catch {}
684
- try { fs.unlinkSync(path.join(engineDir, 'tmp', `sysprompt-${d.id}.md.tmp`)); } catch {}
693
+ try { fs.unlinkSync(path.join(engineDir, 'tmp', `prompt-${d.id}.md`)); } catch { /* cleanup */ }
694
+ try { fs.unlinkSync(path.join(engineDir, 'tmp', `sysprompt-${d.id}.md`)); } catch { /* cleanup */ }
695
+ try { fs.unlinkSync(path.join(engineDir, 'tmp', `sysprompt-${d.id}.md.tmp`)); } catch { /* cleanup */ }
685
696
  }
686
697
  }
687
698
  dispatch[queue] = dispatch[queue].filter(d => !matchFn(d));
@@ -718,7 +729,7 @@ function killEnginePid(pid) {
718
729
  } else {
719
730
  process.kill(pid, 'SIGKILL');
720
731
  }
721
- } catch {}
732
+ } catch { /* process may be dead */ }
722
733
  }
723
734
 
724
735
  function restartEngine() {
@@ -835,7 +846,7 @@ const server = http.createServer(async (req, res) => {
835
846
  dispatch.completed = dispatch.completed.filter(d => !d.meta?.parentKey || d.meta.parentKey !== dispatchKey);
836
847
  return dispatch;
837
848
  }, { defaultValue: { pending: [], active: [], completed: [] } });
838
- } catch {}
849
+ } catch (e) { console.error('dispatch cleanup:', e.message); }
839
850
 
840
851
  // Clear cooldown so item isn't blocked by exponential backoff
841
852
  try {
@@ -845,7 +856,7 @@ const server = http.createServer(async (req, res) => {
845
856
  delete cooldowns[dispatchKey];
846
857
  safeWrite(cooldownPath, cooldowns);
847
858
  }
848
- } catch {}
859
+ } catch (e) { console.error('cooldown cleanup:', e.message); }
849
860
 
850
861
  return jsonReply(res, 200, { ok: true, id });
851
862
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
@@ -894,7 +905,7 @@ const server = http.createServer(async (req, res) => {
894
905
  if (key.includes(id)) { delete cooldowns[key]; cleaned = true; }
895
906
  }
896
907
  if (cleaned) safeWrite(cooldownPath, cooldowns);
897
- } catch {}
908
+ } catch (e) { console.error('cooldown cleanup:', e.message); }
898
909
 
899
910
  invalidateStatusCache();
900
911
  return jsonReply(res, 200, { ok: true, id, dispatchRemoved });
@@ -1153,7 +1164,7 @@ const server = http.createServer(async (req, res) => {
1153
1164
  safeWrite(wiPath, items);
1154
1165
  workItemSynced = true;
1155
1166
  }
1156
- } catch {}
1167
+ } catch (e) { console.error('work item sync:', e.message); }
1157
1168
  }
1158
1169
 
1159
1170
  return jsonReply(res, 200, { ok: true, item, workItemSynced });
@@ -1185,7 +1196,7 @@ const server = http.createServer(async (req, res) => {
1185
1196
  safeWrite(wiPath, filtered);
1186
1197
  cancelled = true;
1187
1198
  }
1188
- } catch {}
1199
+ } catch (e) { console.error('work item cleanup:', e.message); }
1189
1200
  }
1190
1201
  // Also check central work-items
1191
1202
  const centralPath = path.join(MINIONS_DIR, 'work-items.json');
@@ -1194,7 +1205,7 @@ const server = http.createServer(async (req, res) => {
1194
1205
  const before = items.length;
1195
1206
  const filtered = items.filter(w => !(w.sourcePlan === body.source && w.id === body.itemId));
1196
1207
  if (filtered.length < before) { safeWrite(centralPath, filtered); cancelled = true; }
1197
- } catch {}
1208
+ } catch (e) { console.error('central work item cleanup:', e.message); }
1198
1209
 
1199
1210
  // Clean dispatch entries for this item
1200
1211
  cleanDispatchEntries(d =>
@@ -1224,16 +1235,16 @@ const server = http.createServer(async (req, res) => {
1224
1235
  const status = JSON.parse(safeRead(statusPath) || '{}');
1225
1236
  if (status.pid) {
1226
1237
  if (process.platform === 'win32') {
1227
- try { require('child_process').execSync('taskkill /PID ' + status.pid + ' /F /T', { stdio: 'pipe', timeout: 5000 }); } catch {}
1238
+ try { require('child_process').execSync('taskkill /PID ' + status.pid + ' /F /T', { stdio: 'pipe', timeout: 5000 }); } catch { /* process may be dead */ }
1228
1239
  } else {
1229
- try { process.kill(status.pid, 'SIGTERM'); } catch {}
1240
+ try { process.kill(status.pid, 'SIGTERM'); } catch { /* process may be dead */ }
1230
1241
  }
1231
1242
  }
1232
1243
  status.status = 'idle';
1233
1244
  delete status.currentTask;
1234
1245
  delete status.dispatched;
1235
1246
  safeWrite(statusPath, status);
1236
- } catch {}
1247
+ } catch (e) { console.error('agent cancel:', e.message); }
1237
1248
 
1238
1249
  cancelled.push({ agent: d.agent, task: d.task });
1239
1250
  }
@@ -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
@@ -1281,7 +1291,7 @@ const server = http.createServer(async (req, res) => {
1281
1291
  res.write(`data: ${JSON.stringify(content)}\n\n`);
1282
1292
  offset = Buffer.byteLength(content, 'utf8');
1283
1293
  }
1284
- } catch {}
1294
+ } catch { /* optional */ }
1285
1295
 
1286
1296
  // Watch for changes using fs.watchFile (cross-platform, works on Windows)
1287
1297
  const watcher = () => {
@@ -1296,7 +1306,7 @@ const server = http.createServer(async (req, res) => {
1296
1306
  const chunk = buf.toString('utf8');
1297
1307
  if (chunk) res.write(`data: ${JSON.stringify(chunk)}\n\n`);
1298
1308
  }
1299
- } catch {}
1309
+ } catch { /* optional */ }
1300
1310
  };
1301
1311
 
1302
1312
  fs.watchFile(liveLogPath, { interval: 500 }, watcher);
@@ -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));
@@ -1487,7 +1497,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1487
1497
  const meta = `<!-- swept: ${new Date().toISOString()} | reason: ${reason} -->\n`;
1488
1498
  safeWrite(destPath, meta + content);
1489
1499
  safeUnlink(filePath);
1490
- } catch {}
1500
+ } catch (e) { console.error('kb archive:', e.message); }
1491
1501
  }
1492
1502
 
1493
1503
  // Process removals (stale/empty) — archive, not delete
@@ -1524,7 +1534,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1524
1534
  safeWrite(path.join(destDir, entry.file), updated);
1525
1535
  safeUnlink(srcPath);
1526
1536
  reclassified++;
1527
- } catch {}
1537
+ } catch (e) { console.error('kb reclassify:', e.message); }
1528
1538
  }
1529
1539
 
1530
1540
  // Prune swept files older than 30 days
@@ -1535,9 +1545,9 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1535
1545
  const fp = path.join(kbArchiveDir, f);
1536
1546
  try {
1537
1547
  if (Date.now() - fs.statSync(fp).mtimeMs > SWEPT_RETENTION_MS) { safeUnlink(fp); pruned++; }
1538
- } catch {}
1548
+ } catch { /* cleanup */ }
1539
1549
  }
1540
- } catch {}
1550
+ } catch { /* optional */ }
1541
1551
 
1542
1552
  const summary = `${merged} duplicates merged, ${removed} stale removed, ${reclassified} reclassified${pruned ? ', ' + pruned + ' old swept files pruned' : ''}`;
1543
1553
  safeWrite(path.join(ENGINE_DIR, 'kb-swept.json'), JSON.stringify({ timestamp: new Date().toISOString(), summary }));
@@ -1565,7 +1575,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1565
1575
  const filePath = path.join(dir, f);
1566
1576
  const content = safeRead(filePath) || '';
1567
1577
  let updatedAt = '';
1568
- try { updatedAt = new Date(fs.statSync(filePath).mtimeMs).toISOString(); } catch {}
1578
+ try { updatedAt = new Date(fs.statSync(filePath).mtimeMs).toISOString(); } catch { /* optional */ }
1569
1579
  const isJson = f.endsWith('.json');
1570
1580
  if (isJson) {
1571
1581
  try {
@@ -1587,7 +1597,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1587
1597
  revisionFeedback: plan.revision_feedback || null,
1588
1598
  sourcePlan: plan.source_plan || null,
1589
1599
  });
1590
- } catch {}
1600
+ } catch { /* JSON parse fallback */ }
1591
1601
  } else {
1592
1602
  const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
1593
1603
  const projectMatch = content.match(/\*\*Project:\*\*\s*(.+)/m);
@@ -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));
@@ -1645,7 +1655,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1645
1655
  if (!content) return jsonReply(res, 404, { error: 'not found' });
1646
1656
  // Find the actual file path for Last-Modified header + expose resolved relative path
1647
1657
  const planCandidates = [resolvePlanPath(file), path.join(PRD_DIR, file), path.join(PRD_DIR, 'guides', file), path.join(PLANS_DIR, file), path.join(PRD_DIR, 'archive', file), path.join(PLANS_DIR, 'archive', file)];
1648
- for (const p of planCandidates) { try { const st = fs.statSync(p); if (st) { res.setHeader('Last-Modified', st.mtime.toISOString()); res.setHeader('X-Resolved-Path', path.relative(MINIONS_DIR, p).replace(/\\/g, '/')); break; } } catch {} }
1658
+ for (const p of planCandidates) { try { const st = fs.statSync(p); if (st) { res.setHeader('Last-Modified', st.mtime.toISOString()); res.setHeader('X-Resolved-Path', path.relative(MINIONS_DIR, p).replace(/\\/g, '/')); break; } } catch { /* optional */ } }
1649
1659
  const contentType = file.endsWith('.json') ? 'application/json' : 'text/plain';
1650
1660
  res.setHeader('Content-Type', contentType + '; charset=utf-8');
1651
1661
  res.setHeader('Cache-Control', 'no-cache');
@@ -1689,7 +1699,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1689
1699
  }
1690
1700
  }
1691
1701
  if (changed) safeWrite(wiPath, items);
1692
- } catch {}
1702
+ } catch (e) { console.error('resume work items:', e.message); }
1693
1703
  }
1694
1704
 
1695
1705
  // Clear dispatch completed entries for resumed items so they aren't dedup-blocked
@@ -1749,16 +1759,16 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1749
1759
  const agentStatus = JSON.parse(safeRead(statusPath) || '{}');
1750
1760
  if (agentStatus.pid) {
1751
1761
  if (process.platform === 'win32') {
1752
- try { require('child_process').execSync('taskkill /PID ' + agentStatus.pid + ' /F /T', { stdio: 'pipe', timeout: 5000 }); } catch {}
1762
+ try { require('child_process').execSync('taskkill /PID ' + agentStatus.pid + ' /F /T', { stdio: 'pipe', timeout: 5000 }); } catch { /* process may be dead */ }
1753
1763
  } else {
1754
- try { process.kill(agentStatus.pid, 'SIGTERM'); } catch {}
1764
+ try { process.kill(agentStatus.pid, 'SIGTERM'); } catch { /* process may be dead */ }
1755
1765
  }
1756
1766
  }
1757
1767
  agentStatus.status = 'idle';
1758
1768
  delete agentStatus.currentTask;
1759
1769
  delete agentStatus.dispatched;
1760
1770
  safeWrite(statusPath, agentStatus);
1761
- } catch {}
1771
+ } catch (e) { console.error('agent reset:', e.message); }
1762
1772
  killedAgents.add(activeEntry.agent);
1763
1773
  }
1764
1774
  }
@@ -1775,7 +1785,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1775
1785
  if (w.id) resetItemIds.add(w.id);
1776
1786
  }
1777
1787
  if (changed) safeWrite(wiPath, items);
1778
- } catch {}
1788
+ } catch (e) { console.error('reset work items:', e.message); }
1779
1789
  }
1780
1790
 
1781
1791
  // Remove dispatch active entries for reset items or killed agents.
@@ -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);
@@ -1832,7 +1842,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1832
1842
  }
1833
1843
 
1834
1844
  // Delete old PRD — agent will write replacement at same path
1835
- try { fs.unlinkSync(prdPath); } catch {}
1845
+ try { fs.unlinkSync(prdPath); } catch { /* cleanup */ }
1836
1846
 
1837
1847
  // Queue plan-to-prd regeneration with instructions to preserve completed items
1838
1848
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
@@ -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' });
@@ -1952,7 +1963,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1952
1963
  if (filtered.length < items.length) {
1953
1964
  safeWrite(wiInfo.path, filtered);
1954
1965
  }
1955
- } catch {}
1966
+ } catch (e) { console.error('work item sync:', e.message); }
1956
1967
  }
1957
1968
 
1958
1969
  // Count plan items that have no work item yet (will auto-materialize)
@@ -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);
@@ -2001,7 +2012,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
2001
2012
  cleaned += items.length - filtered.length;
2002
2013
  safeWrite(wiPath, filtered);
2003
2014
  }
2004
- } catch {}
2015
+ } catch (e) { console.error('plan cleanup:', e.message); }
2005
2016
  }
2006
2017
 
2007
2018
  // Clean up dispatch entries for this plan's items
@@ -2025,7 +2036,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
2025
2036
  }
2026
2037
  }
2027
2038
  if (changed) safeWrite(centralPath, centralItems);
2028
- } catch {}
2039
+ } catch (e) { console.error('plan-to-prd cleanup:', e.message); }
2029
2040
  }
2030
2041
 
2031
2042
  invalidateStatusCache();
@@ -2166,7 +2177,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
2166
2177
  }
2167
2178
  }
2168
2179
  if (filtered.length < items.length) safeWrite(wiInfo.path, filtered);
2169
- } catch {}
2180
+ } catch (e) { console.error('work item deletion:', e.message); }
2170
2181
  }
2171
2182
  for (const itemId of deletedItemIds) {
2172
2183
  cleanDispatchEntries(d =>
@@ -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);
@@ -2373,7 +2385,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2373
2385
  // Move to archive
2374
2386
  const archiveDir = path.join(MINIONS_DIR, 'notes', 'archive');
2375
2387
  if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
2376
- try { const _c = safeRead(inboxPath); safeWrite(path.join(archiveDir, `persisted-${name}`), _c); safeUnlink(inboxPath); } catch {}
2388
+ try { const _c = safeRead(inboxPath); safeWrite(path.join(archiveDir, `persisted-${name}`), _c); safeUnlink(inboxPath); } catch (e) { console.error('inbox archive:', e.message); }
2377
2389
 
2378
2390
  return jsonReply(res, 200, { ok: true, title });
2379
2391
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
@@ -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
  }
@@ -2410,7 +2423,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2410
2423
  // Move inbox item to archive
2411
2424
  const archiveDir = path.join(MINIONS_DIR, 'notes', 'archive');
2412
2425
  if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
2413
- try { const _c = safeRead(inboxPath); safeWrite(path.join(archiveDir, `kb-${category}-${name}`), _c); safeUnlink(inboxPath); } catch {}
2426
+ try { const _c = safeRead(inboxPath); safeWrite(path.join(archiveDir, `kb-${category}-${name}`), _c); safeUnlink(inboxPath); } catch (e) { console.error('inbox archive:', e.message); }
2414
2427
 
2415
2428
  return jsonReply(res, 200, { ok: true, category, file: name });
2416
2429
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
@@ -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) {
@@ -2513,7 +2526,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2513
2526
  fs.writeFileSync(psPath, psScript);
2514
2527
  try {
2515
2528
  selectedPath = execSync(`powershell -STA -NoProfile -ExecutionPolicy Bypass -File "${psPath}"`, { encoding: 'utf8', timeout: 120000 }).trim();
2516
- } finally { try { fs.unlinkSync(psPath); } catch {} }
2529
+ } finally { try { fs.unlinkSync(psPath); } catch { /* cleanup */ } }
2517
2530
  } else if (process.platform === 'darwin') {
2518
2531
  selectedPath = execSync(`osascript -e 'POSIX path of (choose folder with prompt "Select project folder")'`, { encoding: 'utf8', timeout: 120000 }).trim();
2519
2532
  } else {
@@ -2559,14 +2572,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2559
2572
  remoteUrl.match(/https:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/\s]+)/);
2560
2573
  if (m) { detected.org = m[1]; detected.project = m[2]; detected.repoName = m[3]; }
2561
2574
  }
2562
- } catch {}
2575
+ } catch (e) { console.error('git remote detection:', e.message); }
2563
2576
  try {
2564
2577
  const pkgPath = path.join(target, 'package.json');
2565
2578
  if (fs.existsSync(pkgPath)) {
2566
2579
  const pkg = safeJson(pkgPath);
2567
2580
  if (pkg.name) detected.name = pkg.name.replace(/^@[^/]+\//, '');
2568
2581
  }
2569
- } catch {}
2582
+ } catch { /* optional */ }
2570
2583
  let description = '';
2571
2584
  try {
2572
2585
  const claudeMd = path.join(target, 'CLAUDE.md');
@@ -2574,7 +2587,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2574
2587
  const lines = (safeRead(claudeMd) || '').split('\n').filter(l => l.trim() && !l.startsWith('#'));
2575
2588
  if (lines[0] && lines[0].length < 200) description = lines[0].trim();
2576
2589
  }
2577
- } catch {}
2590
+ } catch { /* optional */ }
2578
2591
 
2579
2592
  const name = body.name || detected.name;
2580
2593
  const prUrlBase = detected.repoHost === 'github'
@@ -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' });
@@ -3028,7 +3042,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3028
3042
 
3029
3043
  // Also append to live-output.log so it shows in the chat view
3030
3044
  const liveLogPath = path.join(agentDir, 'live-output.log');
3031
- try { fs.appendFileSync(liveLogPath, '\n[human-steering] ' + message + '\n'); } catch {}
3045
+ try { fs.appendFileSync(liveLogPath, '\n[human-steering] ' + message + '\n'); } catch { /* optional */ }
3032
3046
 
3033
3047
  return jsonReply(res, 200, { ok: true, message: 'Steering message sent' });
3034
3048
  }},
@@ -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/ado.js CHANGED
@@ -91,7 +91,7 @@ async function forEachActivePr(config, token, callback) {
91
91
  const updated = await callback(project, pr, prNum, orgBase);
92
92
  if (updated) projectUpdated++;
93
93
  } catch (err) {
94
- try { engine().log('warn', `Failed to poll status for ${pr.id}: ${err.message}`); } catch {}
94
+ try { engine().log('warn', `Failed to poll status for ${pr.id}: ${err.message}`); } catch { /* engine not available */ }
95
95
  }
96
96
  }
97
97
 
@@ -160,7 +160,7 @@ async function pollPrStatus(config) {
160
160
  if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
161
161
  else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
162
162
  shared.safeWrite(metricsPath, metrics);
163
- } catch {}
163
+ } catch (err) { try { engine().log('warn', `Metrics update: ${err.message}`); } catch { /* engine not available */ } }
164
164
  }
165
165
  }
166
166
  }