monomind 1.11.14 → 1.13.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.
Files changed (179) hide show
  1. package/.claude/agents/generated/channel-intelligence-director.md +87 -0
  2. package/.claude/agents/generated/chief-growth-officer.md +88 -0
  3. package/.claude/agents/generated/content-seo-strategist.md +90 -0
  4. package/.claude/agents/generated/developer-community-strategist.md +91 -0
  5. package/.claude/agents/generated/outreach-partnership-strategist.md +90 -0
  6. package/.claude/agents/generated/social-media-strategist.md +91 -0
  7. package/.claude/agents/generated/video-visual-strategist.md +90 -0
  8. package/.claude/commands/mastermind/master.md +1 -1
  9. package/.claude/helpers/auto-memory-hook.mjs +13 -4
  10. package/.claude/helpers/control-start.cjs +5 -0
  11. package/.claude/helpers/event-logger.cjs +114 -0
  12. package/.claude/helpers/handlers/adr-draft-handler.cjs +19 -5
  13. package/.claude/helpers/handlers/agent-start-handler.cjs +13 -4
  14. package/.claude/helpers/handlers/compact-handler.cjs +2 -0
  15. package/.claude/helpers/handlers/edit-handler.cjs +1 -1
  16. package/.claude/helpers/handlers/gates-handler.cjs +3 -0
  17. package/.claude/helpers/handlers/graph-status-handler.cjs +14 -8
  18. package/.claude/helpers/handlers/loops-status-handler.cjs +5 -2
  19. package/.claude/helpers/handlers/route-handler.cjs +24 -10
  20. package/.claude/helpers/handlers/session-handler.cjs +11 -4
  21. package/.claude/helpers/handlers/session-restore-handler.cjs +35 -19
  22. package/.claude/helpers/handlers/task-handler.cjs +13 -5
  23. package/.claude/helpers/hook-handler.cjs +40 -0
  24. package/.claude/helpers/intelligence.cjs +130 -53
  25. package/.claude/helpers/loop-tracker.cjs +15 -3
  26. package/.claude/helpers/memory-palace.cjs +461 -0
  27. package/.claude/helpers/memory.cjs +138 -14
  28. package/.claude/helpers/metrics-db.mjs +87 -0
  29. package/.claude/helpers/router.cjs +300 -42
  30. package/.claude/helpers/session.cjs +89 -30
  31. package/.claude/helpers/statusline.cjs +148 -4
  32. package/.claude/helpers/toggle-statusline.cjs +73 -0
  33. package/.claude/helpers/token-tracker.cjs +934 -0
  34. package/.claude/helpers/utils/micro-agents.cjs +20 -4
  35. package/.claude/helpers/utils/monograph.cjs +39 -4
  36. package/.claude/helpers/utils/telemetry.cjs +3 -3
  37. package/.claude/scheduled_tasks.lock +1 -1
  38. package/.claude/settings.json +92 -1
  39. package/.claude/skills/mastermind/_protocol.md +25 -15
  40. package/.claude/skills/mastermind/architect.md +3 -3
  41. package/.claude/skills/mastermind/autodev.md +4 -2
  42. package/.claude/skills/mastermind/idea.md +10 -0
  43. package/.claude/skills/mastermind/ops.md +3 -3
  44. package/.claude/skills/mastermind/runorg.md +153 -86
  45. package/package.json +20 -3
  46. package/packages/@monomind/cli/dist/src/agents/registry-builder.js +2 -0
  47. package/packages/@monomind/cli/dist/src/autopilot-state.js +10 -5
  48. package/packages/@monomind/cli/dist/src/benchmarks/benchmark-runner.js +13 -0
  49. package/packages/@monomind/cli/dist/src/benchmarks/metric-evaluators.js +20 -9
  50. package/packages/@monomind/cli/dist/src/browser/actions.js +10 -3
  51. package/packages/@monomind/cli/dist/src/browser/browser.js +12 -2
  52. package/packages/@monomind/cli/dist/src/browser/cdp.js +21 -3
  53. package/packages/@monomind/cli/dist/src/browser/har.js +27 -5
  54. package/packages/@monomind/cli/dist/src/commands/agent.js +11 -8
  55. package/packages/@monomind/cli/dist/src/commands/analyze.js +36 -21
  56. package/packages/@monomind/cli/dist/src/commands/autopilot.js +12 -4
  57. package/packages/@monomind/cli/dist/src/commands/benchmark.js +51 -8
  58. package/packages/@monomind/cli/dist/src/commands/browse.js +5 -2
  59. package/packages/@monomind/cli/dist/src/commands/claims.js +29 -11
  60. package/packages/@monomind/cli/dist/src/commands/cleanup.js +25 -5
  61. package/packages/@monomind/cli/dist/src/commands/config.js +15 -7
  62. package/packages/@monomind/cli/dist/src/commands/daemon.js +6 -0
  63. package/packages/@monomind/cli/dist/src/commands/deployment.js +34 -19
  64. package/packages/@monomind/cli/dist/src/commands/doctor.js +192 -23
  65. package/packages/@monomind/cli/dist/src/commands/guidance.js +15 -2
  66. package/packages/@monomind/cli/dist/src/commands/hive-mind.js +37 -14
  67. package/packages/@monomind/cli/dist/src/commands/hooks.js +42 -25
  68. package/packages/@monomind/cli/dist/src/commands/init.js +9 -4
  69. package/packages/@monomind/cli/dist/src/commands/issues.js +29 -26
  70. package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -5
  71. package/packages/@monomind/cli/dist/src/commands/memory.js +10 -0
  72. package/packages/@monomind/cli/dist/src/commands/migrate.js +5 -5
  73. package/packages/@monomind/cli/dist/src/commands/monograph.js +18 -5
  74. package/packages/@monomind/cli/dist/src/commands/monovector/backup.js +8 -2
  75. package/packages/@monomind/cli/dist/src/commands/monovector/benchmark.js +20 -7
  76. package/packages/@monomind/cli/dist/src/commands/monovector/import.js +15 -0
  77. package/packages/@monomind/cli/dist/src/commands/monovector/migrate.js +4 -1
  78. package/packages/@monomind/cli/dist/src/commands/monovector/optimize.js +11 -0
  79. package/packages/@monomind/cli/dist/src/commands/monovector/setup.js +11 -1
  80. package/packages/@monomind/cli/dist/src/commands/neural.js +1 -1
  81. package/packages/@monomind/cli/dist/src/commands/performance.js +20 -7
  82. package/packages/@monomind/cli/dist/src/commands/platforms.js +90 -8
  83. package/packages/@monomind/cli/dist/src/commands/plugins.js +12 -5
  84. package/packages/@monomind/cli/dist/src/commands/process.js +33 -10
  85. package/packages/@monomind/cli/dist/src/commands/progress.js +5 -3
  86. package/packages/@monomind/cli/dist/src/commands/providers.js +5 -5
  87. package/packages/@monomind/cli/dist/src/commands/replay.js +8 -2
  88. package/packages/@monomind/cli/dist/src/commands/route.js +27 -7
  89. package/packages/@monomind/cli/dist/src/commands/security.js +4 -0
  90. package/packages/@monomind/cli/dist/src/commands/session.js +12 -1
  91. package/packages/@monomind/cli/dist/src/commands/start.js +11 -4
  92. package/packages/@monomind/cli/dist/src/commands/status.js +7 -4
  93. package/packages/@monomind/cli/dist/src/commands/swarm.js +27 -13
  94. package/packages/@monomind/cli/dist/src/commands/task.js +26 -11
  95. package/packages/@monomind/cli/dist/src/commands/tokens.js +7 -2
  96. package/packages/@monomind/cli/dist/src/commands/transfer-store.js +36 -22
  97. package/packages/@monomind/cli/dist/src/commands/update.js +15 -3
  98. package/packages/@monomind/cli/dist/src/commands/workflow.js +39 -6
  99. package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +18 -7
  100. package/packages/@monomind/cli/dist/src/consensus/vote-signer.js +25 -8
  101. package/packages/@monomind/cli/dist/src/index.js +7 -3
  102. package/packages/@monomind/cli/dist/src/init/executor.js +14 -11
  103. package/packages/@monomind/cli/dist/src/init/shared-instructions-generator.js +20 -4
  104. package/packages/@monomind/cli/dist/src/init/statusline-generator.js +36 -15
  105. package/packages/@monomind/cli/dist/src/mcp-tools/a2a-tools.js +98 -13
  106. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +16 -3
  107. package/packages/@monomind/cli/dist/src/mcp-tools/analyze-tools.js +80 -17
  108. package/packages/@monomind/cli/dist/src/mcp-tools/browser-tools.js +84 -22
  109. package/packages/@monomind/cli/dist/src/mcp-tools/claims-tools.js +35 -7
  110. package/packages/@monomind/cli/dist/src/mcp-tools/config-tools.js +82 -17
  111. package/packages/@monomind/cli/dist/src/mcp-tools/coordination-tools.js +37 -4
  112. package/packages/@monomind/cli/dist/src/mcp-tools/daa-tools.js +49 -7
  113. package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +45 -18
  114. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +75 -25
  115. package/packages/@monomind/cli/dist/src/mcp-tools/guidance-tools.js +32 -10
  116. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +91 -20
  117. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-tools.js +188 -29
  118. package/packages/@monomind/cli/dist/src/mcp-tools/memory-tools.js +25 -7
  119. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +11 -2
  120. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +476 -62
  121. package/packages/@monomind/cli/dist/src/mcp-tools/neural-tools.js +44 -9
  122. package/packages/@monomind/cli/dist/src/mcp-tools/performance-tools.js +45 -10
  123. package/packages/@monomind/cli/dist/src/mcp-tools/progress-tools.js +7 -4
  124. package/packages/@monomind/cli/dist/src/mcp-tools/request-tracker.js +15 -1
  125. package/packages/@monomind/cli/dist/src/mcp-tools/security-tools.js +61 -9
  126. package/packages/@monomind/cli/dist/src/mcp-tools/session-tools.js +45 -14
  127. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +15 -3
  128. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +14 -7
  129. package/packages/@monomind/cli/dist/src/mcp-tools/task-tools.js +52 -10
  130. package/packages/@monomind/cli/dist/src/mcp-tools/terminal-tools.js +40 -6
  131. package/packages/@monomind/cli/dist/src/mcp-tools/transfer-tools.js +37 -4
  132. package/packages/@monomind/cli/dist/src/mcp-tools/workflow-tools.js +29 -6
  133. package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.js +26 -10
  134. package/packages/@monomind/cli/dist/src/memory/intelligence.js +80 -19
  135. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +21 -2
  136. package/packages/@monomind/cli/dist/src/memory/memory-initializer.js +67 -3
  137. package/packages/@monomind/cli/dist/src/memory/sona-optimizer.js +14 -4
  138. package/packages/@monomind/cli/dist/src/monovector/command-outcomes.js +43 -7
  139. package/packages/@monomind/cli/dist/src/monovector/coverage-router.js +8 -4
  140. package/packages/@monomind/cli/dist/src/monovector/coverage-tools.js +6 -3
  141. package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +13 -0
  142. package/packages/@monomind/cli/dist/src/monovector/route-outcomes.d.ts +2 -1
  143. package/packages/@monomind/cli/dist/src/monovector/route-outcomes.js +46 -4
  144. package/packages/@monomind/cli/dist/src/plugins/manager.js +8 -3
  145. package/packages/@monomind/cli/dist/src/plugins/store/discovery.js +46 -2
  146. package/packages/@monomind/cli/dist/src/plugins/store/search.js +5 -4
  147. package/packages/@monomind/cli/dist/src/production/circuit-breaker.js +17 -3
  148. package/packages/@monomind/cli/dist/src/production/error-handler.js +3 -0
  149. package/packages/@monomind/cli/dist/src/production/monitoring.js +20 -3
  150. package/packages/@monomind/cli/dist/src/production/rate-limiter.js +13 -4
  151. package/packages/@monomind/cli/dist/src/production/retry.js +17 -9
  152. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +6 -2
  153. package/packages/@monomind/cli/dist/src/routing/embedder.js +0 -0
  154. package/packages/@monomind/cli/dist/src/routing/llm-caller.js +13 -2
  155. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +18 -3
  156. package/packages/@monomind/cli/dist/src/services/claim-service.d.ts +1 -0
  157. package/packages/@monomind/cli/dist/src/services/claim-service.js +8 -0
  158. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +14 -2
  159. package/packages/@monomind/cli/dist/src/services/headless-worker-executor.js +18 -2
  160. package/packages/@monomind/cli/dist/src/services/worker-daemon.js +348 -17
  161. package/packages/@monomind/cli/dist/src/transfer/anonymization/index.d.ts +0 -3
  162. package/packages/@monomind/cli/dist/src/transfer/anonymization/index.js +16 -1
  163. package/packages/@monomind/cli/dist/src/transfer/export.js +8 -0
  164. package/packages/@monomind/cli/dist/src/transfer/ipfs/upload.js +33 -3
  165. package/packages/@monomind/cli/dist/src/transfer/serialization/cfp.js +8 -2
  166. package/packages/@monomind/cli/dist/src/transfer/storage/gcs.js +37 -3
  167. package/packages/@monomind/cli/dist/src/transfer/store/discovery.js +45 -3
  168. package/packages/@monomind/cli/dist/src/transfer/store/download.js +5 -0
  169. package/packages/@monomind/cli/dist/src/transfer/store/publish.js +13 -1
  170. package/packages/@monomind/cli/dist/src/transfer/store/registry.d.ts +8 -0
  171. package/packages/@monomind/cli/dist/src/transfer/store/registry.js +30 -5
  172. package/packages/@monomind/cli/dist/src/transfer/store/search.js +20 -5
  173. package/packages/@monomind/cli/dist/src/update/checker.js +59 -7
  174. package/packages/@monomind/cli/dist/src/update/executor.js +50 -3
  175. package/packages/@monomind/cli/dist/src/update/index.js +18 -1
  176. package/packages/@monomind/cli/dist/src/update/rate-limiter.d.ts +6 -0
  177. package/packages/@monomind/cli/dist/src/update/rate-limiter.js +79 -7
  178. package/packages/@monomind/cli/dist/src/update/validator.js +52 -1
  179. package/packages/@monomind/cli/package.json +2 -3
@@ -11,6 +11,8 @@
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
13
  import { safeParseEmbedding } from './memory-bridge.js';
14
+ /** Maximum SQLite database file size accepted before read (256 MB). */
15
+ const MAX_DB_FILE_BYTES = 256 * 1024 * 1024;
14
16
  // ADR-053: Lazy import of AgentDB v1 bridge
15
17
  let _bridge;
16
18
  async function getBridge() {
@@ -733,6 +735,11 @@ export async function ensureSchemaColumns(dbPath) {
733
735
  }
734
736
  const initSqlJs = (await import('sql.js')).default;
735
737
  const SQL = await initSqlJs();
738
+ // Guard against excessively large DB files to prevent OOM.
739
+ const ensureStat = fs.statSync(dbPath);
740
+ if (ensureStat.size > MAX_DB_FILE_BYTES) {
741
+ return { success: false, columnsAdded, error: `Database file too large: ${ensureStat.size} bytes` };
742
+ }
736
743
  const fileBuffer = fs.readFileSync(dbPath);
737
744
  const db = new SQL.Database(fileBuffer);
738
745
  // Get current columns in memory_entries
@@ -802,6 +809,14 @@ export async function checkAndMigrateLegacy(options) {
802
809
  try {
803
810
  const initSqlJs = (await import('sql.js')).default;
804
811
  const SQL = await initSqlJs();
812
+ // Guard against excessively large legacy DB files to prevent OOM.
813
+ const legacyStat = fs.statSync(legacyPath);
814
+ if (legacyStat.size > MAX_DB_FILE_BYTES) {
815
+ if (verbose) {
816
+ console.warn(`[memory] Skipping legacy DB at ${legacyPath}: file too large (${legacyStat.size} bytes)`);
817
+ }
818
+ continue;
819
+ }
805
820
  const legacyBuffer = fs.readFileSync(legacyPath);
806
821
  const legacyDb = new SQL.Database(legacyBuffer);
807
822
  // Check if it has data
@@ -1080,6 +1095,11 @@ export async function checkMemoryInitialization(dbPath) {
1080
1095
  // Try to load with sql.js
1081
1096
  const initSqlJs = (await import('sql.js')).default;
1082
1097
  const SQL = await initSqlJs();
1098
+ // Guard against excessively large DB files to prevent OOM.
1099
+ const checkStat = fs.statSync(path_);
1100
+ if (checkStat.size > MAX_DB_FILE_BYTES) {
1101
+ return { initialized: false };
1102
+ }
1083
1103
  const fileBuffer = fs.readFileSync(path_);
1084
1104
  const db = new SQL.Database(fileBuffer);
1085
1105
  // Check for metadata table
@@ -1125,6 +1145,11 @@ export async function applyTemporalDecay(dbPath) {
1125
1145
  try {
1126
1146
  const initSqlJs = (await import('sql.js')).default;
1127
1147
  const SQL = await initSqlJs();
1148
+ // Guard against excessively large DB files to prevent OOM.
1149
+ const decayStat = fs.statSync(path_);
1150
+ if (decayStat.size > MAX_DB_FILE_BYTES) {
1151
+ return { success: false, patternsDecayed: 0, error: `Database file too large: ${decayStat.size} bytes` };
1152
+ }
1128
1153
  const fileBuffer = fs.readFileSync(path_);
1129
1154
  const db = new SQL.Database(fileBuffer);
1130
1155
  // Apply decay: confidence *= exp(-decay_rate * days_since_last_use)
@@ -1445,6 +1470,11 @@ export async function verifyMemoryInit(dbPath, options) {
1445
1470
  const initSqlJs = (await import('sql.js')).default;
1446
1471
  const SQL = await initSqlJs();
1447
1472
  const fs = await import('fs');
1473
+ // Guard against excessively large DB files to prevent OOM.
1474
+ const verifyStat = fs.statSync(dbPath);
1475
+ if (verifyStat.size > MAX_DB_FILE_BYTES) {
1476
+ return { success: false, tests: [{ name: 'Database access', passed: false, details: `File too large: ${verifyStat.size} bytes` }], summary: { passed: 0, failed: 1, total: 1 } };
1477
+ }
1448
1478
  // Load database
1449
1479
  const fileBuffer = fs.readFileSync(dbPath);
1450
1480
  const db = new SQL.Database(fileBuffer);
@@ -1631,6 +1661,11 @@ export async function storeEntry(options) {
1631
1661
  await ensureSchemaColumns(dbPath);
1632
1662
  const initSqlJs = (await import('sql.js')).default;
1633
1663
  const SQL = await initSqlJs();
1664
+ // Guard against excessively large DB files to prevent OOM.
1665
+ const storeStat = fs.statSync(dbPath);
1666
+ if (storeStat.size > MAX_DB_FILE_BYTES) {
1667
+ return { success: false, id: '', error: `Database file too large: ${storeStat.size} bytes` };
1668
+ }
1634
1669
  const fileBuffer = fs.readFileSync(dbPath);
1635
1670
  const db = new SQL.Database(fileBuffer);
1636
1671
  const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
@@ -1727,6 +1762,11 @@ export async function searchEntries(options) {
1727
1762
  }
1728
1763
  // Ensure schema has all required columns (migration for older DBs)
1729
1764
  await ensureSchemaColumns(dbPath);
1765
+ // Guard against excessively large DB files to prevent OOM.
1766
+ const searchStat = fs.statSync(dbPath);
1767
+ if (searchStat.size > MAX_DB_FILE_BYTES) {
1768
+ return { success: false, results: [], searchTime: 0, error: `Database file too large: ${searchStat.size} bytes` };
1769
+ }
1730
1770
  // Generate query embedding
1731
1771
  const queryEmb = await generateEmbedding(query);
1732
1772
  const queryEmbedding = queryEmb.embedding;
@@ -1744,6 +1784,11 @@ export async function searchEntries(options) {
1744
1784
  // Fall back to brute-force SQLite search
1745
1785
  const initSqlJs = (await import('sql.js')).default;
1746
1786
  const SQL = await initSqlJs();
1787
+ // Guard against excessively large DB files to prevent OOM.
1788
+ const searchFbStat = fs.statSync(dbPath);
1789
+ if (searchFbStat.size > MAX_DB_FILE_BYTES) {
1790
+ return { success: false, results: [], searchTime: Date.now() - startTime, error: `Database file too large: ${searchFbStat.size} bytes` };
1791
+ }
1747
1792
  const fileBuffer = fs.readFileSync(dbPath);
1748
1793
  const db = new SQL.Database(fileBuffer);
1749
1794
  // Get entries with embeddings
@@ -1854,6 +1899,11 @@ export async function listEntries(options) {
1854
1899
  await ensureSchemaColumns(dbPath);
1855
1900
  const initSqlJs = (await import('sql.js')).default;
1856
1901
  const SQL = await initSqlJs();
1902
+ // Guard against excessively large DB files to prevent OOM.
1903
+ const listStat = fs.statSync(dbPath);
1904
+ if (listStat.size > MAX_DB_FILE_BYTES) {
1905
+ return { success: false, entries: [], total: 0, error: `Database file too large: ${listStat.size} bytes` };
1906
+ }
1857
1907
  const fileBuffer = fs.readFileSync(dbPath);
1858
1908
  const db = new SQL.Database(fileBuffer);
1859
1909
  // Get total count
@@ -1870,9 +1920,13 @@ export async function listEntries(options) {
1870
1920
  countStmt.free();
1871
1921
  const countResult = countRows.length > 0 ? [{ values: countRows }] : [];
1872
1922
  const total = countResult[0]?.values?.[0]?.[0] || 0;
1873
- // Get entries
1874
- const safeLimit = parseInt(String(limit), 10) || 100;
1875
- const safeOffset = parseInt(String(offset), 10) || 0;
1923
+ // Get entries — cap limit to 10 000 to prevent full-table loads that OOM
1924
+ // the sql.js in-memory database on large datasets.
1925
+ const MAX_LIST_LIMIT = 10_000;
1926
+ const rawLimit = parseInt(String(limit), 10);
1927
+ const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, MAX_LIST_LIMIT) : 100;
1928
+ const rawOffset = parseInt(String(offset), 10);
1929
+ const safeOffset = Number.isFinite(rawOffset) && rawOffset >= 0 ? rawOffset : 0;
1876
1930
  const listStmt = namespace
1877
1931
  ? db.prepare(`SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at FROM memory_entries WHERE status = 'active' AND namespace = ? ORDER BY updated_at DESC LIMIT ? OFFSET ?`)
1878
1932
  : db.prepare(`SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at FROM memory_entries WHERE status = 'active' ORDER BY updated_at DESC LIMIT ? OFFSET ?`);
@@ -1939,6 +1993,11 @@ export async function getEntry(options) {
1939
1993
  await ensureSchemaColumns(dbPath);
1940
1994
  const initSqlJs = (await import('sql.js')).default;
1941
1995
  const SQL = await initSqlJs();
1996
+ // Guard against excessively large DB files to prevent OOM.
1997
+ const getStat = fs.statSync(dbPath);
1998
+ if (getStat.size > MAX_DB_FILE_BYTES) {
1999
+ return { success: false, found: false, error: `Database file too large: ${getStat.size} bytes` };
2000
+ }
1942
2001
  const fileBuffer = fs.readFileSync(dbPath);
1943
2002
  const db = new SQL.Database(fileBuffer);
1944
2003
  // Find entry by key
@@ -2041,6 +2100,11 @@ export async function deleteEntry(options) {
2041
2100
  await ensureSchemaColumns(dbPath);
2042
2101
  const initSqlJs = (await import('sql.js')).default;
2043
2102
  const SQL = await initSqlJs();
2103
+ // Guard against excessively large DB files to prevent OOM.
2104
+ const deleteStat = fs.statSync(dbPath);
2105
+ if (deleteStat.size > MAX_DB_FILE_BYTES) {
2106
+ return { success: false, deleted: false, key, namespace, remainingEntries: 0, error: `Database file too large: ${deleteStat.size} bytes` };
2107
+ }
2044
2108
  const fileBuffer = fs.readFileSync(dbPath);
2045
2109
  const db = new SQL.Database(fileBuffer);
2046
2110
  // Check if entry exists first
@@ -11,7 +11,7 @@
11
11
  * - Persists patterns to .swarm/sona-patterns.json
12
12
  * @module v1/cli/memory/sona-optimizer
13
13
  */
14
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
14
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
15
15
  import { dirname, join } from 'path';
16
16
  // ============================================================================
17
17
  // Constants
@@ -473,6 +473,8 @@ export class SONAOptimizer {
473
473
  if (!existsSync(fullPath)) {
474
474
  return false;
475
475
  }
476
+ if (statSync(fullPath).size > 50 * 1024 * 1024)
477
+ return false;
476
478
  const data = readFileSync(fullPath, 'utf-8');
477
479
  const state = JSON.parse(data);
478
480
  // Validate version
@@ -480,9 +482,13 @@ export class SONAOptimizer {
480
482
  console.error('[SONA] Incompatible state version, starting fresh');
481
483
  return false;
482
484
  }
483
- // Load patterns
485
+ // Load patterns — also cap key length so a crafted state file cannot
486
+ // store arbitrarily long keys that bloat the in-memory Map. The key is
487
+ // an agent:keyword string; 512 chars is ample for any real value.
484
488
  this.patterns.clear();
485
489
  for (const [key, pattern] of Object.entries(state.patterns)) {
490
+ if (typeof key !== 'string' || key.length > 512)
491
+ continue;
486
492
  if (this.validatePattern(pattern)) {
487
493
  this.patterns.set(key, pattern);
488
494
  }
@@ -497,7 +503,9 @@ export class SONAOptimizer {
497
503
  return true;
498
504
  }
499
505
  catch (err) {
500
- console.error(`[SONA] Failed to load state: ${err}`);
506
+ // Strip filesystem paths from error before logging to prevent path disclosure
507
+ const msg = err instanceof Error ? err.message : String(err);
508
+ console.error(`[SONA] Failed to load state: ${msg.replace(/\/[^\s:]+(\/|(?=\s|:|$))/g, '<path>/').slice(0, 200)}`);
501
509
  return false;
502
510
  }
503
511
  }
@@ -532,7 +540,9 @@ export class SONAOptimizer {
532
540
  return true;
533
541
  }
534
542
  catch (err) {
535
- console.error(`[SONA] Failed to save state: ${err}`);
543
+ // Strip filesystem paths from error before logging to prevent path disclosure
544
+ const msg = err instanceof Error ? err.message : String(err);
545
+ console.error(`[SONA] Failed to save state: ${msg.replace(/\/[^\s:]+(\/|(?=\s|:|$))/g, '<path>/').slice(0, 200)}`);
536
546
  return false;
537
547
  }
538
548
  }
@@ -3,19 +3,43 @@
3
3
  * success signal from real command exit codes, instead of trusting a
4
4
  * caller-supplied --success flag.
5
5
  */
6
- import { promises as fs } from 'node:fs';
6
+ import { promises as fs, statSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
+ /** Refuse to read files larger than this to prevent OOM. */
9
+ const MAX_FILE_BYTES = 50 * 1024 * 1024; // 50 MB
10
+ /** Cap command string length to prevent file bloat. */
11
+ const MAX_COMMAND_LEN = 500;
8
12
  function storePath(baseDir) {
9
13
  return join(baseDir, 'command-outcomes.jsonl');
10
14
  }
15
+ /** Maximum number of command-outcome records to keep.
16
+ * deriveRecentSuccess only uses records within a 5-minute window (typically < 50), so
17
+ * anything older is dead weight. 500 gives a comfortable buffer. */
18
+ const MAX_COMMAND_RECORDS = 500;
11
19
  /** Append a command outcome. Non-fatal on error. */
12
20
  export async function recordCommand(baseDir, cmd) {
13
21
  try {
14
22
  await fs.mkdir(baseDir, { recursive: true });
15
- const rec = { ts: cmd.ts, command: cmd.command, exitCode: cmd.exitCode, success: cmd.exitCode === 0 };
16
- await fs.appendFile(storePath(baseDir), JSON.stringify(rec) + '\n', 'utf8');
17
- // Opportunistic trim: keep file bounded (last 500 lines) to avoid unbounded growth
18
- // (cheap: only rewrite when it gets large)
23
+ const path = storePath(baseDir);
24
+ // Cap command length to prevent individual records from bloating the file.
25
+ const safeCommand = cmd.command.length > MAX_COMMAND_LEN ? cmd.command.slice(0, MAX_COMMAND_LEN) : cmd.command;
26
+ const rec = { ts: cmd.ts, command: safeCommand, exitCode: cmd.exitCode, success: cmd.exitCode === 0 };
27
+ await fs.appendFile(path, JSON.stringify(rec) + '\n', 'utf8');
28
+ // Opportunistic trim: rewrite only when the file exceeds the cap.
29
+ // Avoids an extra stat() on every call by catching the overcount lazily.
30
+ // Guard with size check first to prevent OOM on unexpectedly large files.
31
+ try {
32
+ if (statSync(path).size > MAX_FILE_BYTES)
33
+ return;
34
+ }
35
+ catch {
36
+ return;
37
+ }
38
+ const content = await fs.readFile(path, 'utf8').catch(() => '');
39
+ const lines = content.trim().split('\n').filter(Boolean);
40
+ if (lines.length > MAX_COMMAND_RECORDS) {
41
+ await fs.writeFile(path, lines.slice(-MAX_COMMAND_RECORDS).join('\n') + '\n', 'utf8');
42
+ }
19
43
  }
20
44
  catch { /* non-fatal */ }
21
45
  }
@@ -42,7 +66,13 @@ export async function recordCommand(baseDir, cmd) {
42
66
  */
43
67
  export async function deriveRecentSuccess(baseDir, windowMs = 300_000) {
44
68
  try {
45
- const content = await fs.readFile(storePath(baseDir), 'utf8').catch(() => '');
69
+ const p = storePath(baseDir);
70
+ try {
71
+ if (statSync(p).size > MAX_FILE_BYTES)
72
+ return null;
73
+ }
74
+ catch { /* file absent */ }
75
+ const content = await fs.readFile(p, 'utf8').catch(() => '');
46
76
  if (!content)
47
77
  return null;
48
78
  const now = Date.now();
@@ -67,7 +97,13 @@ export async function deriveRecentSuccess(baseDir, windowMs = 300_000) {
67
97
  /** Read recent command outcomes (for diagnostics). */
68
98
  export async function readCommandOutcomes(baseDir, windowMs = 300_000) {
69
99
  try {
70
- const content = await fs.readFile(storePath(baseDir), 'utf8').catch(() => '');
100
+ const p = storePath(baseDir);
101
+ try {
102
+ if (statSync(p).size > MAX_FILE_BYTES)
103
+ return [];
104
+ }
105
+ catch { /* file absent */ }
106
+ const content = await fs.readFile(p, 'utf8').catch(() => '');
71
107
  if (!content)
72
108
  return [];
73
109
  const now = Date.now();
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * @module @monomind/cli/monovector/coverage-router
12
12
  */
13
- import { existsSync, readFileSync } from 'fs';
13
+ import { existsSync, readFileSync, statSync } from 'fs';
14
14
  import { join } from 'path';
15
15
  import { getProjectCwd } from '../mcp-tools/types.js';
16
16
  // ============================================================================
@@ -28,6 +28,10 @@ const EMPTY = {
28
28
  overallStatementCoverage: 0,
29
29
  },
30
30
  };
31
+ /** Maximum bytes for a JSON coverage summary (20 MB). */
32
+ const MAX_COVERAGE_JSON_BYTES = 20 * 1024 * 1024;
33
+ /** Maximum bytes for an lcov.info file (50 MB). */
34
+ const MAX_COVERAGE_LCOV_BYTES = 50 * 1024 * 1024;
31
35
  /**
32
36
  * Read coverage data from disk. Checks, in order:
33
37
  * 1. coverage/coverage-summary.json (Jest/Istanbul)
@@ -39,7 +43,7 @@ const EMPTY = {
39
43
  export function readCoverage(cwd = getProjectCwd()) {
40
44
  for (const rel of ['coverage/coverage-summary.json', 'coverage-summary.json']) {
41
45
  const p = join(cwd, rel);
42
- if (existsSync(p)) {
46
+ if (existsSync(p) && statSync(p).size <= MAX_COVERAGE_JSON_BYTES) {
43
47
  try {
44
48
  return parseSummaryJson(JSON.parse(readFileSync(p, 'utf-8')), rel);
45
49
  }
@@ -50,7 +54,7 @@ export function readCoverage(cwd = getProjectCwd()) {
50
54
  }
51
55
  for (const rel of ['coverage/lcov.info', 'lcov.info']) {
52
56
  const p = join(cwd, rel);
53
- if (existsSync(p)) {
57
+ if (existsSync(p) && statSync(p).size <= MAX_COVERAGE_LCOV_BYTES) {
54
58
  try {
55
59
  return parseLcov(readFileSync(p, 'utf-8'), rel);
56
60
  }
@@ -60,7 +64,7 @@ export function readCoverage(cwd = getProjectCwd()) {
60
64
  }
61
65
  }
62
66
  const nyc = join(cwd, '.nyc_output', 'out.json');
63
- if (existsSync(nyc)) {
67
+ if (existsSync(nyc) && statSync(nyc).size <= MAX_COVERAGE_JSON_BYTES) {
64
68
  try {
65
69
  return parseSummaryJson(JSON.parse(readFileSync(nyc, 'utf-8')), '.nyc_output/out.json');
66
70
  }
@@ -35,7 +35,8 @@ export const coverageRouterTools = [
35
35
  },
36
36
  handler: async (input) => {
37
37
  try {
38
- const result = await coverageRoute(input.path || '', { threshold: input.threshold ?? 80 });
38
+ const rawPath = typeof input.path === 'string' ? input.path.slice(0, 512) : '';
39
+ const result = await coverageRoute(rawPath, { threshold: input.threshold ?? 80 });
39
40
  if (!result.found) {
40
41
  return text('No coverage report found. Run your test suite with coverage enabled (e.g. `vitest run --coverage`), then retry.');
41
42
  }
@@ -66,7 +67,7 @@ export const coverageRouterTools = [
66
67
  try {
67
68
  const result = await coverageGaps({
68
69
  threshold: input.threshold ?? 80,
69
- path: input.path || undefined,
70
+ path: typeof input.path === 'string' ? input.path.slice(0, 512) : undefined,
70
71
  groupByAgent: true,
71
72
  });
72
73
  if (!result.found)
@@ -96,7 +97,9 @@ export const coverageRouterTools = [
96
97
  },
97
98
  handler: async (input) => {
98
99
  try {
99
- const result = await coverageSuggest(input.path || '.', { threshold: input.threshold ?? 80, limit: input.limit ?? 20 });
100
+ const rawPath = typeof input.path === 'string' ? input.path.slice(0, 512) : '.';
101
+ const rawLimit = typeof input.limit === 'number' ? Math.min(Math.max(1, input.limit), 200) : 20;
102
+ const result = await coverageSuggest(rawPath, { threshold: input.threshold ?? 80, limit: rawLimit });
100
103
  if (!result.found) {
101
104
  return text('No coverage report found. Run your test suite with coverage enabled, then retry.');
102
105
  }
@@ -626,8 +626,11 @@ export function suggestReviewers(files, fileRisks) {
626
626
  return Array.from(reviewers).slice(0, 5);
627
627
  }
628
628
  // Analysis result cache
629
+ // FIFO eviction cap matches diffCache. Without a cap, repeated calls to analyzeDiff
630
+ // with unique refs (e.g. HEAD~0...HEAD~N) would grow this Map to GBs.
629
631
  const analysisCache = new Map();
630
632
  const ANALYSIS_CACHE_TTL_MS = 3000; // 3 seconds
633
+ const ANALYSIS_CACHE_MAX_ENTRIES = 50;
631
634
  /**
632
635
  * Analyze a diff with full analysis (optimized with caching)
633
636
  */
@@ -662,6 +665,11 @@ export async function analyzeDiff(options) {
662
665
  recommendedReviewers,
663
666
  };
664
667
  // Cache the result
668
+ if (analysisCache.size >= ANALYSIS_CACHE_MAX_ENTRIES) {
669
+ const oldestKey = analysisCache.keys().next().value;
670
+ if (oldestKey !== undefined)
671
+ analysisCache.delete(oldestKey);
672
+ }
665
673
  analysisCache.set(ref, { result, timestamp: Date.now() });
666
674
  return result;
667
675
  }
@@ -692,6 +700,11 @@ export function analyzeDiffSync(options) {
692
700
  fileRisks,
693
701
  recommendedReviewers,
694
702
  };
703
+ if (analysisCache.size >= ANALYSIS_CACHE_MAX_ENTRIES) {
704
+ const oldestKey = analysisCache.keys().next().value;
705
+ if (oldestKey !== undefined)
706
+ analysisCache.delete(oldestKey);
707
+ }
695
708
  analysisCache.set(ref, { result, timestamp: Date.now() });
696
709
  return result;
697
710
  }
@@ -10,7 +10,8 @@ export interface RouteOutcomeRecord {
10
10
  measuredSuccess?: boolean;
11
11
  quality?: number;
12
12
  }
13
- /** Append a route recommendation (pre-outcome). */
13
+ /** Append a route recommendation (pre-outcome). Opportunistically trims the
14
+ * file to MAX_ROUTE_RECORDS lines to prevent unbounded growth. */
14
15
  export declare function recordRoute(baseDir: string, rec: RouteOutcomeRecord): Promise<void>;
15
16
  /** Join outcome data onto the most recent matching route record by routeId. */
16
17
  export declare function joinOutcome(baseDir: string, routeId: string, outcome: {
@@ -3,16 +3,42 @@
3
3
  * what actually happened. This is the foundation for routing-accuracy metrics
4
4
  * and for giving SONA a real training label.
5
5
  */
6
- import { promises as fs } from 'node:fs';
6
+ import { promises as fs, statSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
+ /** Refuse to read files larger than this to prevent OOM. */
9
+ const MAX_FILE_BYTES = 50 * 1024 * 1024; // 50 MB
10
+ /** Cap string fields stored in each record to prevent file bloat. */
11
+ const MAX_FIELD_LEN = 500;
8
12
  function storePath(baseDir) {
9
13
  return join(baseDir, 'route-outcomes.jsonl');
10
14
  }
11
- /** Append a route recommendation (pre-outcome). */
15
+ /** Maximum number of records to keep in route-outcomes.jsonl.
16
+ * computeRoutingAccuracy only ever reads the last 100 records, so anything
17
+ * older is dead weight. Keeping 500 gives a comfortable buffer while bounding
18
+ * the file size and keeping joinOutcome's full-file rewrite cheap. */
19
+ const MAX_ROUTE_RECORDS = 500;
20
+ /** Append a route recommendation (pre-outcome). Opportunistically trims the
21
+ * file to MAX_ROUTE_RECORDS lines to prevent unbounded growth. */
12
22
  export async function recordRoute(baseDir, rec) {
13
23
  try {
14
24
  await fs.mkdir(baseDir, { recursive: true });
15
- await fs.appendFile(storePath(baseDir), JSON.stringify(rec) + '\n', 'utf8');
25
+ const path = storePath(baseDir);
26
+ // Cap string fields to prevent individual records from bloating the file.
27
+ const safeRec = {
28
+ ...rec,
29
+ routeId: rec.routeId.slice(0, MAX_FIELD_LEN),
30
+ task: rec.task.slice(0, MAX_FIELD_LEN),
31
+ recommendedAgent: rec.recommendedAgent.slice(0, MAX_FIELD_LEN),
32
+ routingMethod: rec.routingMethod.slice(0, 64),
33
+ };
34
+ await fs.appendFile(path, JSON.stringify(safeRec) + '\n', 'utf8');
35
+ // Opportunistic trim: rewrite only when the file exceeds the cap.
36
+ // Avoids an extra stat() on every call by catching the overcount lazily.
37
+ const content = await fs.readFile(path, 'utf8').catch(() => '');
38
+ const lines = content.trim().split('\n').filter(Boolean);
39
+ if (lines.length > MAX_ROUTE_RECORDS) {
40
+ await fs.writeFile(path, lines.slice(-MAX_ROUTE_RECORDS).join('\n') + '\n', 'utf8');
41
+ }
16
42
  }
17
43
  catch {
18
44
  // Non-fatal — telemetry must never break routing
@@ -22,6 +48,11 @@ export async function recordRoute(baseDir, rec) {
22
48
  export async function joinOutcome(baseDir, routeId, outcome) {
23
49
  try {
24
50
  const path = storePath(baseDir);
51
+ try {
52
+ if (statSync(path).size > MAX_FILE_BYTES)
53
+ return;
54
+ }
55
+ catch { /* file absent */ }
25
56
  const content = await fs.readFile(path, 'utf8').catch(() => '');
26
57
  if (!content)
27
58
  return;
@@ -52,6 +83,11 @@ export async function joinLatestUnresolved(baseDir, outcome, maxAgeMs = 600_000
52
83
  ) {
53
84
  try {
54
85
  const path = storePath(baseDir);
86
+ try {
87
+ if (statSync(path).size > MAX_FILE_BYTES)
88
+ return null;
89
+ }
90
+ catch { /* file absent */ }
55
91
  const content = await fs.readFile(path, 'utf8').catch(() => '');
56
92
  if (!content)
57
93
  return null;
@@ -81,7 +117,13 @@ export async function joinLatestUnresolved(baseDir, outcome, maxAgeMs = 600_000
81
117
  /** Read all outcome records (for metrics). */
82
118
  export async function readOutcomes(baseDir) {
83
119
  try {
84
- const content = await fs.readFile(storePath(baseDir), 'utf8').catch(() => '');
120
+ const p = storePath(baseDir);
121
+ try {
122
+ if (statSync(p).size > MAX_FILE_BYTES)
123
+ return [];
124
+ }
125
+ catch { /* file absent */ }
126
+ const content = await fs.readFile(p, 'utf8').catch(() => '');
85
127
  if (!content)
86
128
  return [];
87
129
  return content.trim().split('\n').map(l => {
@@ -73,7 +73,8 @@ export class PluginManager {
73
73
  }
74
74
  async loadManifest() {
75
75
  try {
76
- if (fs.existsSync(this.config.manifestPath)) {
76
+ const MAX_MANIFEST_BYTES = 10 * 1024 * 1024; // 10 MB
77
+ if (fs.existsSync(this.config.manifestPath) && fs.statSync(this.config.manifestPath).size <= MAX_MANIFEST_BYTES) {
77
78
  const content = fs.readFileSync(this.config.manifestPath, 'utf-8');
78
79
  const parsed = JSON.parse(content);
79
80
  if (Object.prototype.hasOwnProperty.call(parsed, '__proto__') ||
@@ -148,7 +149,8 @@ export class PluginManager {
148
149
  let installedVersion = version || 'latest';
149
150
  let commands = [];
150
151
  let hooks = [];
151
- if (fs.existsSync(packageJsonPath)) {
152
+ const MAX_PKG_JSON_BYTES = 1024 * 1024; // 1 MB
153
+ if (fs.existsSync(packageJsonPath) && fs.statSync(packageJsonPath).size <= MAX_PKG_JSON_BYTES) {
152
154
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
153
155
  installedVersion = pkg.version;
154
156
  // Check for monomind plugin metadata
@@ -205,6 +207,9 @@ export class PluginManager {
205
207
  if (!fs.existsSync(packageJsonPath)) {
206
208
  return { success: false, error: 'No package.json found at path' };
207
209
  }
210
+ if (fs.statSync(packageJsonPath).size > 1024 * 1024) {
211
+ return { success: false, error: 'package.json exceeds size limit' };
212
+ }
208
213
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
209
214
  const packageName = pkg.name;
210
215
  if (!isValidPluginKey(packageName)) {
@@ -423,7 +428,7 @@ export class PluginManager {
423
428
  // Update manifest
424
429
  const installDir = path.join(this.config.pluginsDir, 'node_modules');
425
430
  const packageJsonPath = path.join(installDir, packageName, 'package.json');
426
- if (fs.existsSync(packageJsonPath)) {
431
+ if (fs.existsSync(packageJsonPath) && fs.statSync(packageJsonPath).size <= 1024 * 1024) {
427
432
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
428
433
  existing.version = pkg.version;
429
434
  existing.commands = pkg['monomind']?.commands || existing.commands;
@@ -5,23 +5,67 @@
5
5
  */
6
6
  import * as crypto from 'crypto';
7
7
  import { resolveIPNS, fetchFromIPFS } from '../../transfer/ipfs/client.js';
8
+ /** Maximum bytes accepted from npm API responses to prevent OOM. */
9
+ const NPM_RESPONSE_MAX_BYTES = 256 * 1024; // 256 KB
10
+ /**
11
+ * Read a fetch Response body up to maxBytes. AbortSignal.timeout() only
12
+ * bounds wall-clock time, NOT response body size; a slow-drip or large
13
+ * response would otherwise buffer unbounded memory here.
14
+ */
15
+ async function readBoundedJson(res, maxBytes) {
16
+ const lenHdr = res.headers.get('content-length');
17
+ if (lenHdr) {
18
+ const declared = parseInt(lenHdr, 10);
19
+ if (Number.isFinite(declared) && declared > maxBytes) {
20
+ throw new Error(`npm response too large: ${declared} bytes`);
21
+ }
22
+ }
23
+ if (!res.body)
24
+ return JSON.parse('');
25
+ const reader = res.body.getReader();
26
+ const chunks = [];
27
+ let total = 0;
28
+ while (true) {
29
+ const { done, value } = await reader.read();
30
+ if (done)
31
+ break;
32
+ if (value) {
33
+ total += value.byteLength;
34
+ if (total > maxBytes) {
35
+ await reader.cancel();
36
+ throw new Error(`npm response exceeded ${maxBytes} bytes`);
37
+ }
38
+ chunks.push(value);
39
+ }
40
+ }
41
+ const buf = new Uint8Array(total);
42
+ let off = 0;
43
+ for (const c of chunks) {
44
+ buf.set(c, off);
45
+ off += c.byteLength;
46
+ }
47
+ return JSON.parse(new TextDecoder('utf-8').decode(buf));
48
+ }
8
49
  /**
9
50
  * Fetch real npm download stats for a package
10
51
  */
11
52
  async function fetchNpmStats(packageName) {
53
+ // Validate package name length to avoid constructing huge URLs
54
+ if (!packageName || packageName.length > 214)
55
+ return null;
12
56
  try {
13
57
  // Fetch last week downloads
14
58
  const downloadsUrl = `https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`;
15
59
  const downloadsRes = await fetch(downloadsUrl, { signal: AbortSignal.timeout(3000) });
16
60
  if (!downloadsRes.ok)
17
61
  return null;
18
- const downloadsData = await downloadsRes.json();
62
+ const downloadsData = await readBoundedJson(downloadsRes, NPM_RESPONSE_MAX_BYTES);
19
63
  // Fetch package info for version
20
64
  const packageUrl = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
21
65
  const packageRes = await fetch(packageUrl, { signal: AbortSignal.timeout(3000) });
22
66
  let version = 'unknown';
23
67
  if (packageRes.ok) {
24
- const packageData = await packageRes.json();
68
+ const packageData = await readBoundedJson(packageRes, NPM_RESPONSE_MAX_BYTES);
25
69
  version = packageData.version || 'unknown';
26
70
  }
27
71
  return {
@@ -9,7 +9,7 @@ export function searchPlugins(registry, options = {}) {
9
9
  let plugins = [...registry.plugins];
10
10
  // Text search (name, displayName, description, tags)
11
11
  if (options.query) {
12
- const query = options.query.toLowerCase();
12
+ const query = options.query.slice(0, 256).toLowerCase();
13
13
  plugins = plugins.filter(p => p.name.toLowerCase().includes(query) ||
14
14
  p.displayName.toLowerCase().includes(query) ||
15
15
  p.description.toLowerCase().includes(query) ||
@@ -88,8 +88,8 @@ export function searchPlugins(registry, options = {}) {
88
88
  });
89
89
  // Pagination
90
90
  const total = plugins.length;
91
- const limit = options.limit || 20;
92
- const offset = options.offset || 0;
91
+ const limit = Math.min(Math.max(1, options.limit || 20), 200);
92
+ const offset = Math.max(0, Math.min(options.offset || 0, 100_000));
93
93
  const page = Math.floor(offset / limit) + 1;
94
94
  plugins = plugins.slice(offset, offset + limit);
95
95
  return {
@@ -105,7 +105,8 @@ export function searchPlugins(registry, options = {}) {
105
105
  * Get search suggestions based on partial query
106
106
  */
107
107
  export function getPluginSearchSuggestions(registry, partialQuery, limit = 10) {
108
- const query = partialQuery.toLowerCase();
108
+ const query = partialQuery.slice(0, 256).toLowerCase();
109
+ limit = Math.min(Math.max(1, limit), 100);
109
110
  const suggestions = new Set();
110
111
  // Search in plugin names
111
112
  for (const plugin of registry.plugins) {