@monoes/monomindcli 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 (434) 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/skills/mastermind/_protocol.md +25 -15
  38. package/.claude/skills/mastermind/architect.md +3 -3
  39. package/.claude/skills/mastermind/autodev.md +4 -2
  40. package/.claude/skills/mastermind/idea.md +10 -0
  41. package/.claude/skills/mastermind/ops.md +3 -3
  42. package/.claude/skills/mastermind/runorg.md +153 -86
  43. package/dist/src/agents/registry-builder.d.ts.map +1 -1
  44. package/dist/src/agents/registry-builder.js +2 -0
  45. package/dist/src/agents/registry-builder.js.map +1 -1
  46. package/dist/src/autopilot-state.d.ts.map +1 -1
  47. package/dist/src/autopilot-state.js +10 -5
  48. package/dist/src/autopilot-state.js.map +1 -1
  49. package/dist/src/benchmarks/benchmark-runner.d.ts.map +1 -1
  50. package/dist/src/benchmarks/benchmark-runner.js +13 -0
  51. package/dist/src/benchmarks/benchmark-runner.js.map +1 -1
  52. package/dist/src/benchmarks/metric-evaluators.d.ts.map +1 -1
  53. package/dist/src/benchmarks/metric-evaluators.js +20 -9
  54. package/dist/src/benchmarks/metric-evaluators.js.map +1 -1
  55. package/dist/src/browser/actions.d.ts.map +1 -1
  56. package/dist/src/browser/actions.js +10 -3
  57. package/dist/src/browser/actions.js.map +1 -1
  58. package/dist/src/browser/browser.d.ts.map +1 -1
  59. package/dist/src/browser/browser.js +12 -2
  60. package/dist/src/browser/browser.js.map +1 -1
  61. package/dist/src/browser/cdp.d.ts.map +1 -1
  62. package/dist/src/browser/cdp.js +21 -3
  63. package/dist/src/browser/cdp.js.map +1 -1
  64. package/dist/src/browser/har.d.ts.map +1 -1
  65. package/dist/src/browser/har.js +27 -5
  66. package/dist/src/browser/har.js.map +1 -1
  67. package/dist/src/commands/agent.d.ts.map +1 -1
  68. package/dist/src/commands/agent.js +11 -8
  69. package/dist/src/commands/agent.js.map +1 -1
  70. package/dist/src/commands/analyze.d.ts.map +1 -1
  71. package/dist/src/commands/analyze.js +36 -21
  72. package/dist/src/commands/analyze.js.map +1 -1
  73. package/dist/src/commands/autopilot.d.ts.map +1 -1
  74. package/dist/src/commands/autopilot.js +12 -4
  75. package/dist/src/commands/autopilot.js.map +1 -1
  76. package/dist/src/commands/benchmark.d.ts.map +1 -1
  77. package/dist/src/commands/benchmark.js +51 -8
  78. package/dist/src/commands/benchmark.js.map +1 -1
  79. package/dist/src/commands/browse.d.ts.map +1 -1
  80. package/dist/src/commands/browse.js +5 -2
  81. package/dist/src/commands/browse.js.map +1 -1
  82. package/dist/src/commands/claims.d.ts.map +1 -1
  83. package/dist/src/commands/claims.js +29 -11
  84. package/dist/src/commands/claims.js.map +1 -1
  85. package/dist/src/commands/cleanup.d.ts.map +1 -1
  86. package/dist/src/commands/cleanup.js +25 -5
  87. package/dist/src/commands/cleanup.js.map +1 -1
  88. package/dist/src/commands/config.d.ts.map +1 -1
  89. package/dist/src/commands/config.js +15 -7
  90. package/dist/src/commands/config.js.map +1 -1
  91. package/dist/src/commands/daemon.d.ts.map +1 -1
  92. package/dist/src/commands/daemon.js +6 -0
  93. package/dist/src/commands/daemon.js.map +1 -1
  94. package/dist/src/commands/deployment.d.ts.map +1 -1
  95. package/dist/src/commands/deployment.js +34 -19
  96. package/dist/src/commands/deployment.js.map +1 -1
  97. package/dist/src/commands/doctor.d.ts.map +1 -1
  98. package/dist/src/commands/doctor.js +133 -15
  99. package/dist/src/commands/doctor.js.map +1 -1
  100. package/dist/src/commands/guidance.d.ts.map +1 -1
  101. package/dist/src/commands/guidance.js +15 -2
  102. package/dist/src/commands/guidance.js.map +1 -1
  103. package/dist/src/commands/hive-mind.d.ts.map +1 -1
  104. package/dist/src/commands/hive-mind.js +37 -14
  105. package/dist/src/commands/hive-mind.js.map +1 -1
  106. package/dist/src/commands/hooks.d.ts.map +1 -1
  107. package/dist/src/commands/hooks.js +42 -25
  108. package/dist/src/commands/hooks.js.map +1 -1
  109. package/dist/src/commands/init.d.ts.map +1 -1
  110. package/dist/src/commands/init.js +9 -4
  111. package/dist/src/commands/init.js.map +1 -1
  112. package/dist/src/commands/issues.d.ts.map +1 -1
  113. package/dist/src/commands/issues.js +29 -26
  114. package/dist/src/commands/issues.js.map +1 -1
  115. package/dist/src/commands/mcp.d.ts.map +1 -1
  116. package/dist/src/commands/mcp.js +11 -5
  117. package/dist/src/commands/mcp.js.map +1 -1
  118. package/dist/src/commands/memory.d.ts.map +1 -1
  119. package/dist/src/commands/memory.js +10 -0
  120. package/dist/src/commands/memory.js.map +1 -1
  121. package/dist/src/commands/migrate.js +5 -5
  122. package/dist/src/commands/migrate.js.map +1 -1
  123. package/dist/src/commands/monograph.d.ts.map +1 -1
  124. package/dist/src/commands/monograph.js +18 -5
  125. package/dist/src/commands/monograph.js.map +1 -1
  126. package/dist/src/commands/monovector/backup.d.ts.map +1 -1
  127. package/dist/src/commands/monovector/backup.js +8 -2
  128. package/dist/src/commands/monovector/backup.js.map +1 -1
  129. package/dist/src/commands/monovector/benchmark.d.ts.map +1 -1
  130. package/dist/src/commands/monovector/benchmark.js +20 -7
  131. package/dist/src/commands/monovector/benchmark.js.map +1 -1
  132. package/dist/src/commands/monovector/import.d.ts.map +1 -1
  133. package/dist/src/commands/monovector/import.js +15 -0
  134. package/dist/src/commands/monovector/import.js.map +1 -1
  135. package/dist/src/commands/monovector/migrate.d.ts.map +1 -1
  136. package/dist/src/commands/monovector/migrate.js +4 -1
  137. package/dist/src/commands/monovector/migrate.js.map +1 -1
  138. package/dist/src/commands/monovector/optimize.d.ts.map +1 -1
  139. package/dist/src/commands/monovector/optimize.js +11 -0
  140. package/dist/src/commands/monovector/optimize.js.map +1 -1
  141. package/dist/src/commands/monovector/setup.d.ts.map +1 -1
  142. package/dist/src/commands/monovector/setup.js +11 -1
  143. package/dist/src/commands/monovector/setup.js.map +1 -1
  144. package/dist/src/commands/neural.js +1 -1
  145. package/dist/src/commands/neural.js.map +1 -1
  146. package/dist/src/commands/performance.d.ts.map +1 -1
  147. package/dist/src/commands/performance.js +20 -7
  148. package/dist/src/commands/performance.js.map +1 -1
  149. package/dist/src/commands/platforms.d.ts.map +1 -1
  150. package/dist/src/commands/platforms.js +90 -8
  151. package/dist/src/commands/platforms.js.map +1 -1
  152. package/dist/src/commands/plugins.d.ts.map +1 -1
  153. package/dist/src/commands/plugins.js +12 -5
  154. package/dist/src/commands/plugins.js.map +1 -1
  155. package/dist/src/commands/process.d.ts.map +1 -1
  156. package/dist/src/commands/process.js +33 -10
  157. package/dist/src/commands/process.js.map +1 -1
  158. package/dist/src/commands/progress.d.ts.map +1 -1
  159. package/dist/src/commands/progress.js +5 -3
  160. package/dist/src/commands/progress.js.map +1 -1
  161. package/dist/src/commands/providers.js +5 -5
  162. package/dist/src/commands/providers.js.map +1 -1
  163. package/dist/src/commands/replay.d.ts.map +1 -1
  164. package/dist/src/commands/replay.js +8 -2
  165. package/dist/src/commands/replay.js.map +1 -1
  166. package/dist/src/commands/route.d.ts.map +1 -1
  167. package/dist/src/commands/route.js +27 -7
  168. package/dist/src/commands/route.js.map +1 -1
  169. package/dist/src/commands/security.d.ts.map +1 -1
  170. package/dist/src/commands/security.js +4 -0
  171. package/dist/src/commands/security.js.map +1 -1
  172. package/dist/src/commands/session.d.ts.map +1 -1
  173. package/dist/src/commands/session.js +12 -1
  174. package/dist/src/commands/session.js.map +1 -1
  175. package/dist/src/commands/start.d.ts.map +1 -1
  176. package/dist/src/commands/start.js +11 -4
  177. package/dist/src/commands/start.js.map +1 -1
  178. package/dist/src/commands/status.d.ts.map +1 -1
  179. package/dist/src/commands/status.js +7 -4
  180. package/dist/src/commands/status.js.map +1 -1
  181. package/dist/src/commands/swarm.d.ts.map +1 -1
  182. package/dist/src/commands/swarm.js +27 -13
  183. package/dist/src/commands/swarm.js.map +1 -1
  184. package/dist/src/commands/task.d.ts.map +1 -1
  185. package/dist/src/commands/task.js +26 -11
  186. package/dist/src/commands/task.js.map +1 -1
  187. package/dist/src/commands/tokens.d.ts.map +1 -1
  188. package/dist/src/commands/tokens.js +7 -2
  189. package/dist/src/commands/tokens.js.map +1 -1
  190. package/dist/src/commands/transfer-store.d.ts.map +1 -1
  191. package/dist/src/commands/transfer-store.js +36 -22
  192. package/dist/src/commands/transfer-store.js.map +1 -1
  193. package/dist/src/commands/update.d.ts.map +1 -1
  194. package/dist/src/commands/update.js +15 -3
  195. package/dist/src/commands/update.js.map +1 -1
  196. package/dist/src/commands/workflow.d.ts.map +1 -1
  197. package/dist/src/commands/workflow.js +39 -6
  198. package/dist/src/commands/workflow.js.map +1 -1
  199. package/dist/src/consensus/audit-writer.d.ts.map +1 -1
  200. package/dist/src/consensus/audit-writer.js +18 -7
  201. package/dist/src/consensus/audit-writer.js.map +1 -1
  202. package/dist/src/consensus/vote-signer.d.ts.map +1 -1
  203. package/dist/src/consensus/vote-signer.js +25 -8
  204. package/dist/src/consensus/vote-signer.js.map +1 -1
  205. package/dist/src/index.d.ts.map +1 -1
  206. package/dist/src/index.js +7 -3
  207. package/dist/src/index.js.map +1 -1
  208. package/dist/src/init/executor.d.ts.map +1 -1
  209. package/dist/src/init/executor.js +14 -11
  210. package/dist/src/init/executor.js.map +1 -1
  211. package/dist/src/init/shared-instructions-generator.d.ts.map +1 -1
  212. package/dist/src/init/shared-instructions-generator.js +20 -4
  213. package/dist/src/init/shared-instructions-generator.js.map +1 -1
  214. package/dist/src/init/statusline-generator.d.ts.map +1 -1
  215. package/dist/src/init/statusline-generator.js +36 -15
  216. package/dist/src/init/statusline-generator.js.map +1 -1
  217. package/dist/src/mcp-tools/a2a-tools.d.ts.map +1 -1
  218. package/dist/src/mcp-tools/a2a-tools.js +98 -13
  219. package/dist/src/mcp-tools/a2a-tools.js.map +1 -1
  220. package/dist/src/mcp-tools/agent-tools.d.ts.map +1 -1
  221. package/dist/src/mcp-tools/agent-tools.js +16 -3
  222. package/dist/src/mcp-tools/agent-tools.js.map +1 -1
  223. package/dist/src/mcp-tools/analyze-tools.d.ts.map +1 -1
  224. package/dist/src/mcp-tools/analyze-tools.js +80 -17
  225. package/dist/src/mcp-tools/analyze-tools.js.map +1 -1
  226. package/dist/src/mcp-tools/browser-tools.d.ts.map +1 -1
  227. package/dist/src/mcp-tools/browser-tools.js +84 -22
  228. package/dist/src/mcp-tools/browser-tools.js.map +1 -1
  229. package/dist/src/mcp-tools/claims-tools.d.ts.map +1 -1
  230. package/dist/src/mcp-tools/claims-tools.js +35 -7
  231. package/dist/src/mcp-tools/claims-tools.js.map +1 -1
  232. package/dist/src/mcp-tools/config-tools.d.ts.map +1 -1
  233. package/dist/src/mcp-tools/config-tools.js +82 -17
  234. package/dist/src/mcp-tools/config-tools.js.map +1 -1
  235. package/dist/src/mcp-tools/coordination-tools.d.ts.map +1 -1
  236. package/dist/src/mcp-tools/coordination-tools.js +37 -4
  237. package/dist/src/mcp-tools/coordination-tools.js.map +1 -1
  238. package/dist/src/mcp-tools/daa-tools.d.ts.map +1 -1
  239. package/dist/src/mcp-tools/daa-tools.js +49 -7
  240. package/dist/src/mcp-tools/daa-tools.js.map +1 -1
  241. package/dist/src/mcp-tools/embeddings-tools.d.ts.map +1 -1
  242. package/dist/src/mcp-tools/embeddings-tools.js +45 -18
  243. package/dist/src/mcp-tools/embeddings-tools.js.map +1 -1
  244. package/dist/src/mcp-tools/github-tools.d.ts.map +1 -1
  245. package/dist/src/mcp-tools/github-tools.js +75 -25
  246. package/dist/src/mcp-tools/github-tools.js.map +1 -1
  247. package/dist/src/mcp-tools/guidance-tools.d.ts.map +1 -1
  248. package/dist/src/mcp-tools/guidance-tools.js +32 -10
  249. package/dist/src/mcp-tools/guidance-tools.js.map +1 -1
  250. package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -1
  251. package/dist/src/mcp-tools/hive-mind-tools.js +91 -20
  252. package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
  253. package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
  254. package/dist/src/mcp-tools/hooks-tools.js +188 -29
  255. package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
  256. package/dist/src/mcp-tools/memory-tools.d.ts.map +1 -1
  257. package/dist/src/mcp-tools/memory-tools.js +25 -7
  258. package/dist/src/mcp-tools/memory-tools.js.map +1 -1
  259. package/dist/src/mcp-tools/monograph-compat.d.ts.map +1 -1
  260. package/dist/src/mcp-tools/monograph-compat.js +11 -2
  261. package/dist/src/mcp-tools/monograph-compat.js.map +1 -1
  262. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  263. package/dist/src/mcp-tools/monograph-tools.js +476 -62
  264. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  265. package/dist/src/mcp-tools/neural-tools.d.ts.map +1 -1
  266. package/dist/src/mcp-tools/neural-tools.js +44 -9
  267. package/dist/src/mcp-tools/neural-tools.js.map +1 -1
  268. package/dist/src/mcp-tools/performance-tools.d.ts.map +1 -1
  269. package/dist/src/mcp-tools/performance-tools.js +45 -10
  270. package/dist/src/mcp-tools/performance-tools.js.map +1 -1
  271. package/dist/src/mcp-tools/progress-tools.d.ts.map +1 -1
  272. package/dist/src/mcp-tools/progress-tools.js +7 -4
  273. package/dist/src/mcp-tools/progress-tools.js.map +1 -1
  274. package/dist/src/mcp-tools/request-tracker.d.ts.map +1 -1
  275. package/dist/src/mcp-tools/request-tracker.js +15 -1
  276. package/dist/src/mcp-tools/request-tracker.js.map +1 -1
  277. package/dist/src/mcp-tools/security-tools.d.ts.map +1 -1
  278. package/dist/src/mcp-tools/security-tools.js +61 -9
  279. package/dist/src/mcp-tools/security-tools.js.map +1 -1
  280. package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
  281. package/dist/src/mcp-tools/session-tools.js +45 -14
  282. package/dist/src/mcp-tools/session-tools.js.map +1 -1
  283. package/dist/src/mcp-tools/swarm-tools.d.ts.map +1 -1
  284. package/dist/src/mcp-tools/swarm-tools.js +15 -3
  285. package/dist/src/mcp-tools/swarm-tools.js.map +1 -1
  286. package/dist/src/mcp-tools/system-tools.d.ts.map +1 -1
  287. package/dist/src/mcp-tools/system-tools.js +14 -7
  288. package/dist/src/mcp-tools/system-tools.js.map +1 -1
  289. package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
  290. package/dist/src/mcp-tools/task-tools.js +52 -10
  291. package/dist/src/mcp-tools/task-tools.js.map +1 -1
  292. package/dist/src/mcp-tools/terminal-tools.d.ts.map +1 -1
  293. package/dist/src/mcp-tools/terminal-tools.js +40 -6
  294. package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
  295. package/dist/src/mcp-tools/transfer-tools.d.ts.map +1 -1
  296. package/dist/src/mcp-tools/transfer-tools.js +37 -4
  297. package/dist/src/mcp-tools/transfer-tools.js.map +1 -1
  298. package/dist/src/mcp-tools/workflow-tools.d.ts.map +1 -1
  299. package/dist/src/mcp-tools/workflow-tools.js +29 -6
  300. package/dist/src/mcp-tools/workflow-tools.js.map +1 -1
  301. package/dist/src/memory/ewc-consolidation.d.ts.map +1 -1
  302. package/dist/src/memory/ewc-consolidation.js +26 -10
  303. package/dist/src/memory/ewc-consolidation.js.map +1 -1
  304. package/dist/src/memory/intelligence.d.ts.map +1 -1
  305. package/dist/src/memory/intelligence.js +80 -19
  306. package/dist/src/memory/intelligence.js.map +1 -1
  307. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  308. package/dist/src/memory/memory-bridge.js +21 -2
  309. package/dist/src/memory/memory-bridge.js.map +1 -1
  310. package/dist/src/memory/memory-initializer.d.ts.map +1 -1
  311. package/dist/src/memory/memory-initializer.js +67 -3
  312. package/dist/src/memory/memory-initializer.js.map +1 -1
  313. package/dist/src/memory/sona-optimizer.d.ts.map +1 -1
  314. package/dist/src/memory/sona-optimizer.js +14 -4
  315. package/dist/src/memory/sona-optimizer.js.map +1 -1
  316. package/dist/src/monovector/command-outcomes.d.ts.map +1 -1
  317. package/dist/src/monovector/command-outcomes.js +43 -7
  318. package/dist/src/monovector/command-outcomes.js.map +1 -1
  319. package/dist/src/monovector/coverage-router.d.ts.map +1 -1
  320. package/dist/src/monovector/coverage-router.js +8 -4
  321. package/dist/src/monovector/coverage-router.js.map +1 -1
  322. package/dist/src/monovector/coverage-tools.d.ts.map +1 -1
  323. package/dist/src/monovector/coverage-tools.js +6 -3
  324. package/dist/src/monovector/coverage-tools.js.map +1 -1
  325. package/dist/src/monovector/diff-classifier.d.ts.map +1 -1
  326. package/dist/src/monovector/diff-classifier.js +13 -0
  327. package/dist/src/monovector/diff-classifier.js.map +1 -1
  328. package/dist/src/monovector/route-outcomes.d.ts +2 -1
  329. package/dist/src/monovector/route-outcomes.d.ts.map +1 -1
  330. package/dist/src/monovector/route-outcomes.js +46 -4
  331. package/dist/src/monovector/route-outcomes.js.map +1 -1
  332. package/dist/src/plugins/manager.d.ts.map +1 -1
  333. package/dist/src/plugins/manager.js +8 -3
  334. package/dist/src/plugins/manager.js.map +1 -1
  335. package/dist/src/plugins/store/discovery.d.ts.map +1 -1
  336. package/dist/src/plugins/store/discovery.js +46 -2
  337. package/dist/src/plugins/store/discovery.js.map +1 -1
  338. package/dist/src/plugins/store/search.d.ts.map +1 -1
  339. package/dist/src/plugins/store/search.js +5 -4
  340. package/dist/src/plugins/store/search.js.map +1 -1
  341. package/dist/src/production/circuit-breaker.d.ts.map +1 -1
  342. package/dist/src/production/circuit-breaker.js +17 -3
  343. package/dist/src/production/circuit-breaker.js.map +1 -1
  344. package/dist/src/production/error-handler.d.ts.map +1 -1
  345. package/dist/src/production/error-handler.js +3 -0
  346. package/dist/src/production/error-handler.js.map +1 -1
  347. package/dist/src/production/monitoring.d.ts.map +1 -1
  348. package/dist/src/production/monitoring.js +20 -3
  349. package/dist/src/production/monitoring.js.map +1 -1
  350. package/dist/src/production/rate-limiter.d.ts.map +1 -1
  351. package/dist/src/production/rate-limiter.js +13 -4
  352. package/dist/src/production/rate-limiter.js.map +1 -1
  353. package/dist/src/production/retry.d.ts.map +1 -1
  354. package/dist/src/production/retry.js +17 -9
  355. package/dist/src/production/retry.js.map +1 -1
  356. package/dist/src/routing/embed-worker.js +6 -2
  357. package/dist/src/routing/embed-worker.js.map +1 -1
  358. package/dist/src/routing/embedder.d.ts.map +1 -1
  359. package/dist/src/routing/embedder.js +0 -0
  360. package/dist/src/routing/embedder.js.map +1 -1
  361. package/dist/src/routing/llm-caller.d.ts.map +1 -1
  362. package/dist/src/routing/llm-caller.js +13 -2
  363. package/dist/src/routing/llm-caller.js.map +1 -1
  364. package/dist/src/routing/route-layer-factory.d.ts.map +1 -1
  365. package/dist/src/routing/route-layer-factory.js +18 -3
  366. package/dist/src/routing/route-layer-factory.js.map +1 -1
  367. package/dist/src/services/claim-service.d.ts +1 -0
  368. package/dist/src/services/claim-service.d.ts.map +1 -1
  369. package/dist/src/services/claim-service.js +8 -0
  370. package/dist/src/services/claim-service.js.map +1 -1
  371. package/dist/src/services/config-file-manager.d.ts.map +1 -1
  372. package/dist/src/services/config-file-manager.js +14 -2
  373. package/dist/src/services/config-file-manager.js.map +1 -1
  374. package/dist/src/services/headless-worker-executor.d.ts.map +1 -1
  375. package/dist/src/services/headless-worker-executor.js +18 -2
  376. package/dist/src/services/headless-worker-executor.js.map +1 -1
  377. package/dist/src/services/worker-daemon.d.ts.map +1 -1
  378. package/dist/src/services/worker-daemon.js +348 -17
  379. package/dist/src/services/worker-daemon.js.map +1 -1
  380. package/dist/src/transfer/anonymization/index.d.ts +0 -3
  381. package/dist/src/transfer/anonymization/index.d.ts.map +1 -1
  382. package/dist/src/transfer/anonymization/index.js +16 -1
  383. package/dist/src/transfer/anonymization/index.js.map +1 -1
  384. package/dist/src/transfer/export.d.ts.map +1 -1
  385. package/dist/src/transfer/export.js +8 -0
  386. package/dist/src/transfer/export.js.map +1 -1
  387. package/dist/src/transfer/ipfs/upload.d.ts.map +1 -1
  388. package/dist/src/transfer/ipfs/upload.js +33 -3
  389. package/dist/src/transfer/ipfs/upload.js.map +1 -1
  390. package/dist/src/transfer/serialization/cfp.d.ts.map +1 -1
  391. package/dist/src/transfer/serialization/cfp.js +8 -2
  392. package/dist/src/transfer/serialization/cfp.js.map +1 -1
  393. package/dist/src/transfer/storage/gcs.d.ts.map +1 -1
  394. package/dist/src/transfer/storage/gcs.js +37 -3
  395. package/dist/src/transfer/storage/gcs.js.map +1 -1
  396. package/dist/src/transfer/store/discovery.d.ts.map +1 -1
  397. package/dist/src/transfer/store/discovery.js +45 -3
  398. package/dist/src/transfer/store/discovery.js.map +1 -1
  399. package/dist/src/transfer/store/download.d.ts.map +1 -1
  400. package/dist/src/transfer/store/download.js +5 -0
  401. package/dist/src/transfer/store/download.js.map +1 -1
  402. package/dist/src/transfer/store/publish.d.ts.map +1 -1
  403. package/dist/src/transfer/store/publish.js +13 -1
  404. package/dist/src/transfer/store/publish.js.map +1 -1
  405. package/dist/src/transfer/store/registry.d.ts +8 -0
  406. package/dist/src/transfer/store/registry.d.ts.map +1 -1
  407. package/dist/src/transfer/store/registry.js +30 -5
  408. package/dist/src/transfer/store/registry.js.map +1 -1
  409. package/dist/src/transfer/store/search.d.ts.map +1 -1
  410. package/dist/src/transfer/store/search.js +20 -5
  411. package/dist/src/transfer/store/search.js.map +1 -1
  412. package/dist/src/ui/collector.mjs +39 -5
  413. package/dist/src/ui/dashboard.html +1603 -1284
  414. package/dist/src/ui/orgs.html +722 -12
  415. package/dist/src/ui/server.mjs +717 -136
  416. package/dist/src/update/checker.d.ts.map +1 -1
  417. package/dist/src/update/checker.js +59 -7
  418. package/dist/src/update/checker.js.map +1 -1
  419. package/dist/src/update/executor.d.ts.map +1 -1
  420. package/dist/src/update/executor.js +50 -3
  421. package/dist/src/update/executor.js.map +1 -1
  422. package/dist/src/update/index.d.ts.map +1 -1
  423. package/dist/src/update/index.js +18 -1
  424. package/dist/src/update/index.js.map +1 -1
  425. package/dist/src/update/rate-limiter.d.ts +6 -0
  426. package/dist/src/update/rate-limiter.d.ts.map +1 -1
  427. package/dist/src/update/rate-limiter.js +79 -7
  428. package/dist/src/update/rate-limiter.js.map +1 -1
  429. package/dist/src/update/validator.d.ts.map +1 -1
  430. package/dist/src/update/validator.js +52 -1
  431. package/dist/src/update/validator.js.map +1 -1
  432. package/dist/tsconfig.tsbuildinfo +1 -1
  433. package/package.json +2 -3
  434. package/dist/src/ui/data/mastermind-events.jsonl +0 -59
@@ -11,6 +11,7 @@ const buildDocsState = new Map();
11
11
 
12
12
  // Pricing per token (mirrors token-tracker.cjs FALLBACK_PRICING)
13
13
  const _SJ_PRICING = {
14
+ 'claude-opus-4-8': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
14
15
  'claude-opus-4-6': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
15
16
  'claude-opus-4-5': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
16
17
  'claude-opus-4': { in: 15e-6, out: 75e-6, cw: 18.75e-6, cr: 1.5e-6 },
@@ -20,13 +21,16 @@ const _SJ_PRICING = {
20
21
  'claude-3-7-sonnet': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
21
22
  'claude-3-5-sonnet': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
22
23
  'claude-haiku-4-5': { in: 1e-6, out: 5e-6, cw: 1.25e-6, cr: 0.1e-6 },
24
+ 'claude-haiku-4': { in: 0.8e-6, out: 4e-6, cw: 1e-6, cr: 0.08e-6 },
23
25
  'claude-3-5-haiku': { in: 0.8e-6, out: 4e-6, cw: 1e-6, cr: 0.08e-6 },
24
26
  'gpt-4o': { in: 2.5e-6, out: 10e-6, cw: 2.5e-6, cr: 1.25e-6 },
25
27
  'gpt-4o-mini': { in: 0.15e-6, out: 0.6e-6, cw: 0.15e-6, cr: 0.075e-6 },
26
28
  'gemini-2.5-pro': { in: 1.25e-6, out: 10e-6, cw: 1.25e-6, cr: 0.315e-6 },
27
29
  };
28
30
  function _sjGetPricing(model) {
29
- const canonical = (model || '').replace(/@.*$/, '').replace(/-\d{8}$/, '');
31
+ const _ALIAS = { 'haiku': 'claude-haiku-4-5', 'opus': 'claude-opus-4-6', 'sonnet': 'claude-sonnet-4-6' };
32
+ let canonical = (model || '').replace(/@.*$/, '').replace(/-\d{8}$/, '');
33
+ canonical = _ALIAS[canonical] || canonical;
30
34
  if (_SJ_PRICING[canonical]) return _SJ_PRICING[canonical];
31
35
  for (const k of Object.keys(_SJ_PRICING)) { if (canonical.startsWith(k) || canonical.includes(k)) return _SJ_PRICING[k]; }
32
36
  return null;
@@ -51,7 +55,7 @@ function categorizeTool(name) {
51
55
  if (['Read','Write','Edit','MultiEdit','Glob','Grep','LS'].includes(name)) return 'file';
52
56
  if (name === 'Bash') return 'bash';
53
57
  if (['Agent','Task'].includes(name)) return 'agent';
54
- if (name.startsWith('mcp__monobrain__memory') || name.startsWith('mcp__monobrain__agentdb')) return 'memory';
58
+ if (name.startsWith('mcp__monomind__memory') || name.startsWith('mcp__monomind__agentdb')) return 'memory';
55
59
  if (['WebFetch','WebSearch'].includes(name)) return 'web';
56
60
  if (name === 'TodoWrite' || name === 'TodoRead') return 'task';
57
61
  if (name === 'Skill') return 'skill';
@@ -131,8 +135,8 @@ function buildToolLabel(name, input) {
131
135
  if (name === 'WebFetch') return `Fetch ${(input.url || '').slice(0, 50)}`;
132
136
  if (name === 'WebSearch') return `Search ${(input.query || '').slice(0, 40)}`;
133
137
  if (name === 'Skill') return `Skill: ${input.skill || '?'}`;
134
- if (name.startsWith('mcp__monobrain__memory')) return name.replace('mcp__monobrain__memory_', 'mem:');
135
- if (name.startsWith('mcp__')) return name.replace('mcp__monobrain__', '⬡ ').replace('mcp__', '⬡ ').slice(0, 40);
138
+ if (name.startsWith('mcp__monomind__memory')) return name.replace('mcp__monomind__memory_', 'mem:');
139
+ if (name.startsWith('mcp__')) return name.replace('mcp__monomind__', '⬡ ').replace('mcp__', '⬡ ').slice(0, 40);
136
140
  return name.slice(0, 40);
137
141
  }
138
142
 
@@ -179,6 +183,41 @@ function pathToSections(filename) {
179
183
  const sseClients = new Set();
180
184
  // Mastermind real-time event stream clients
181
185
  const mmSseClients = new Set();
186
+ // Active org run tracking: org -> runId (enables event routing for orgs without runId in payload)
187
+ const activeOrgRuns = new Map();
188
+
189
+ // Returns the shared git directory parent so run files survive branch switches and
190
+ // are shared across all worktrees. In a worktree, .git is a FILE pointing to the
191
+ // shared .git dir (e.g. /main/.git/worktrees/feat); we navigate up two levels to
192
+ // reach /main/.git, then up one more to /main/ for the monomind data root.
193
+ // Falls back to the working directory if git isn't available.
194
+ const _gitMonomindCache = new Map();
195
+ function _getGitMonomindDir(workDir) {
196
+ if (!workDir) return null;
197
+ if (_gitMonomindCache.has(workDir)) return _gitMonomindCache.get(workDir);
198
+ let result = null;
199
+ try {
200
+ const gitEntry = path.join(workDir, '.git');
201
+ const st = fs.statSync(gitEntry);
202
+ if (st.isDirectory()) {
203
+ // Regular repo: .git is a directory
204
+ result = path.join(gitEntry, 'monomind');
205
+ } else if (st.isFile()) {
206
+ // Worktree: .git is a text file "gitdir: /main/.git/worktrees/name"
207
+ const m = fs.readFileSync(gitEntry, 'utf8').trim().match(/^gitdir:\s*(.+)/);
208
+ if (m) {
209
+ // Resolve relative paths (gitdir can be relative to the worktree root)
210
+ const worktreeDir = path.resolve(workDir, m[1].trim());
211
+ // /main/.git/worktrees/name -> /main/.git -> /main/.git/monomind
212
+ const commonGitDir = path.dirname(path.dirname(worktreeDir));
213
+ result = path.join(commonGitDir, 'monomind');
214
+ }
215
+ }
216
+ } catch {}
217
+ if (!result) result = path.join(workDir, '.monomind'); // fallback
218
+ _gitMonomindCache.set(workDir, result);
219
+ return result;
220
+ }
182
221
 
183
222
  // Server state
184
223
  let running = false;
@@ -370,12 +409,22 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
370
409
  const cwd = projectDir || process.cwd();
371
410
  const name = gitExec('git config user.name', { cwd, encoding: 'utf8' }).trim();
372
411
  const email = gitExec('git config user.email', { cwd, encoding: 'utf8' }).trim();
412
+ let remoteUrl = '';
413
+ try { remoteUrl = gitExec('git remote get-url origin', { cwd, encoding: 'utf8' }).trim(); } catch {}
414
+ // Normalise SSH remote to HTTPS URL for browser linking
415
+ if (remoteUrl.startsWith('git@')) {
416
+ remoteUrl = remoteUrl.replace(/^git@([^:]+):/, 'https://$1/').replace(/\.git$/, '');
417
+ } else if (remoteUrl.endsWith('.git')) {
418
+ remoteUrl = remoteUrl.slice(0, -4);
419
+ }
420
+ let branch = '';
421
+ try { branch = gitExec('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8' }).trim(); } catch {}
373
422
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
374
- res.end(JSON.stringify({ name, email, cwd }));
423
+ res.end(JSON.stringify({ name, email, cwd, remoteUrl, branch }));
375
424
  } catch (_) {
376
425
  const cwd2 = projectDir || process.cwd();
377
426
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
378
- res.end(JSON.stringify({ name: '', email: '', cwd: cwd2 }));
427
+ res.end(JSON.stringify({ name: '', email: '', cwd: cwd2, remoteUrl: '', branch: '' }));
379
428
  }
380
429
  return;
381
430
  }
@@ -410,7 +459,23 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
410
459
  return;
411
460
  }
412
461
  try {
413
- const raw = fs.readFileSync(file, 'utf8');
462
+ // Security: validate that the requested file stays within the user's
463
+ // home directory. Without this, ?file=/etc/passwd discloses arbitrary
464
+ // system files to any process that can reach localhost:4242.
465
+ const _resolvedFile = path.resolve(file);
466
+ const _homeDir = os.homedir();
467
+ if (!_resolvedFile.startsWith(_homeDir + path.sep) && !_resolvedFile.startsWith(_homeDir)) {
468
+ res.writeHead(403, { 'Content-Type': 'application/json' });
469
+ res.end(JSON.stringify({ error: 'Access denied: file must be within the home directory' }));
470
+ return;
471
+ }
472
+ // Only allow JSONL files (session logs).
473
+ if (!_resolvedFile.endsWith('.jsonl')) {
474
+ res.writeHead(403, { 'Content-Type': 'application/json' });
475
+ res.end(JSON.stringify({ error: 'Access denied: only .jsonl files are permitted' }));
476
+ return;
477
+ }
478
+ const raw = fs.readFileSync(_resolvedFile, 'utf8');
414
479
  const allLines = raw.split('\n').filter(Boolean);
415
480
  const lines = allLines.slice(-limit);
416
481
  const events = parseSessionLines(lines);
@@ -450,7 +515,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
450
515
  for (const { f, mtime } of sessionFiles) {
451
516
  const fp = path.join(projectClaudeDir, f);
452
517
  const id = f.replace('.jsonl', '');
453
- let lastPrompt = '', summaries = [], totalDurationMs = 0, totalMessages = 0, firstTs = null, lastTs = null, totalCost = 0, toolCalls = 0, userMessages = 0, cacheReadTokens = 0, totalInputTokens = 0;
518
+ let lastPrompt = '', summaries = [], totalDurationMs = 0, totalMessages = 0, firstTs = null, lastTs = null, totalCost = 0, toolCalls = 0, userMessages = 0, cacheReadTokens = 0, totalInputTokens = 0, errorCount = 0;
454
519
  const modelBreakdown = {};
455
520
  const filesTouchedSet = new Set();
456
521
  try {
@@ -460,7 +525,12 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
460
525
  let e; try { e = JSON.parse(line); } catch { continue; }
461
526
  if (e.timestamp) { if (!firstTs) firstTs = e.timestamp; lastTs = e.timestamp; }
462
527
  if (e.type === 'last-prompt' && e.lastPrompt) lastPrompt = e.lastPrompt;
463
- if (e.type === 'user') userMessages++;
528
+ if (e.type === 'user') {
529
+ userMessages++;
530
+ for (const b of (e.message?.content || [])) {
531
+ if (b && b.type === 'tool_result' && b.is_error) errorCount++;
532
+ }
533
+ }
464
534
  if (e.type === 'system' && e.subtype === 'compact_boundary') pendingCompact = true;
465
535
  if (pendingCompact && e.type === 'user') {
466
536
  const msg = e.message || {};
@@ -502,7 +572,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
502
572
  }
503
573
  } catch {}
504
574
  const filesTouched = [...filesTouchedSet].slice(0, 20);
505
- sessions.push({ id, mtime, firstTs, lastTs, lastPrompt, summaries, totalDurationMs, totalMessages, totalCost, toolCalls, userMessages, cacheReadTokens, totalInputTokens, modelBreakdown, filesTouched, file: fp });
575
+ const compactCount = summaries.length;
576
+ const summary = summaries.length ? summaries[summaries.length - 1].text : null;
577
+ sessions.push({ id, mtime, firstTs, lastTs, lastPrompt, summaries, summary, compactCount, errorCount, totalDurationMs, totalMessages, totalCost, toolCalls, userMessages, cacheReadTokens, totalInputTokens, modelBreakdown, filesTouched, file: fp });
506
578
  }
507
579
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
508
580
  res.end(JSON.stringify({ sessions }));
@@ -569,6 +641,69 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
569
641
  return;
570
642
  }
571
643
 
644
+ // ------------------------------------------------------- GET /api/recent-events
645
+ if (req.method === 'GET' && url === '/api/recent-events') {
646
+ try {
647
+ const qs = new URL(req.url, 'http://localhost').searchParams;
648
+ const dir = qs.get('dir') || projectDir || process.cwd();
649
+ const limit = Math.min(parseInt(qs.get('limit') || '50', 10), 200);
650
+ const d = path.resolve(dir || process.cwd());
651
+ const slug = d.replace(/\//g, '-');
652
+ const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
653
+ let sessionFiles = [];
654
+ try {
655
+ sessionFiles = fs.readdirSync(projectClaudeDir)
656
+ .filter(f => f.endsWith('.jsonl'))
657
+ .map(f => { try { return { f, mtime: fs.statSync(path.join(projectClaudeDir, f)).mtimeMs }; } catch { return null; } })
658
+ .filter(Boolean)
659
+ .sort((a, b) => b.mtime - a.mtime)
660
+ .slice(0, 5); // check last 5 sessions
661
+ } catch {}
662
+
663
+ const events = [];
664
+ const HOOK_RE = /^<(local-command-|command-name>|command-message>)/;
665
+ for (const { f } of sessionFiles) {
666
+ const fp = path.join(projectClaudeDir, f);
667
+ const sessId = f.replace('.jsonl', '');
668
+ try {
669
+ const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean).slice(-200);
670
+ for (const line of lines) {
671
+ let e; try { e = JSON.parse(line); } catch { continue; }
672
+ if (e.type === 'assistant') {
673
+ const content = e.message?.content || [];
674
+ for (const block of content) {
675
+ if (block?.type === 'tool_use') {
676
+ events.push({ kind: 'tool', ts: e.timestamp, tool: block.name, session: sessId });
677
+ }
678
+ }
679
+ } else if (e.type === 'user') {
680
+ const content = e.message?.content || [];
681
+ for (const block of content) {
682
+ if (block?.type === 'text' && block.text?.trim() && !HOOK_RE.test(block.text.trim())) {
683
+ events.push({ kind: 'user', ts: e.timestamp, text: block.text.slice(0, 120), session: sessId });
684
+ }
685
+ }
686
+ }
687
+ }
688
+ } catch {}
689
+ }
690
+
691
+ // sort by ts desc, take limit
692
+ events.sort((a, b) => {
693
+ const ta = a.ts ? new Date(a.ts).getTime() : 0;
694
+ const tb = b.ts ? new Date(b.ts).getTime() : 0;
695
+ return tb - ta;
696
+ });
697
+
698
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
699
+ res.end(JSON.stringify({ events: events.slice(0, limit) }));
700
+ } catch (err) {
701
+ res.writeHead(500, { 'Content-Type': 'application/json' });
702
+ res.end(JSON.stringify({ error: err.message }));
703
+ }
704
+ return;
705
+ }
706
+
572
707
  // ------------------------------------------------------- GET /api/tool-errors
573
708
  if (req.method === 'GET' && url === '/api/tool-errors') {
574
709
  try {
@@ -891,7 +1026,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
891
1026
  // ------------------------------------------------------- PUT /api/memory-file
892
1027
  if (req.method === 'PUT' && url === '/api/memory-file') {
893
1028
  let body = '';
894
- req.on('data', chunk => { body += chunk; });
1029
+ req.on('data', chunk => { body += chunk; if (body.length > 2097152) { req.destroy(); return; } });
895
1030
  req.on('end', () => {
896
1031
  try {
897
1032
  const qs = new URL(req.url, 'http://localhost').searchParams;
@@ -925,7 +1060,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
925
1060
  // ------------------------------------------------------- DELETE /api/memory-file
926
1061
  if (req.method === 'DELETE' && url === '/api/memory-file') {
927
1062
  let body = '';
928
- req.on('data', chunk => { body += chunk; });
1063
+ req.on('data', chunk => { body += chunk; if (body.length > 2097152) { req.destroy(); return; } });
929
1064
  req.on('end', () => {
930
1065
  try {
931
1066
  const qs = new URL(req.url, 'http://localhost').searchParams;
@@ -1060,8 +1195,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1060
1195
  let alive = false;
1061
1196
  try { process.kill(pid, 0); alive = true; } catch {}
1062
1197
  const alreadyTracked = loops.some(l => l.id === sessionId || l.sessionId === sessionId);
1063
- const hasRepeatLoops = loops.some(l => l.source === '_repeat.md');
1064
- if (alive && sessionId && !alreadyTracked && !hasRepeatLoops && !stopFiles.has(sessionId)) {
1198
+ if (alive && sessionId && !alreadyTracked && !stopFiles.has(sessionId)) {
1065
1199
  // Try to extract ScheduleWakeup context from session JSONL
1066
1200
  let loopEntry = null;
1067
1201
  try {
@@ -1144,6 +1278,10 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1144
1278
  }
1145
1279
  } catch {}
1146
1280
 
1281
+ // Dedup: suppress scheduled_tasks_lock noise when real repeat loops exist
1282
+ const hasRepeatLoops = loops.some(l => l.source !== 'scheduled_tasks_lock' && l.source !== 'schedule_wakeup_hook');
1283
+ if (hasRepeatLoops) loops = loops.filter(l => l.source !== 'scheduled_tasks_lock' && l.source !== 'schedule_wakeup_hook');
1284
+
1147
1285
  loops.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
1148
1286
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1149
1287
  res.end(JSON.stringify({ loops }));
@@ -1154,13 +1292,14 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1154
1292
  // ---------------------------------------------------------- POST /api/loops/stop
1155
1293
  if (req.method === 'POST' && url === '/api/loops/stop') {
1156
1294
  let body = '';
1157
- req.on('data', chunk => { body += chunk; });
1295
+ req.on('data', chunk => { body += chunk; if (body.length > 2097152) { req.destroy(); return; } });
1158
1296
  req.on('end', () => {
1159
1297
  try {
1160
- const stopQs = new URL(req.url, 'http://localhost').searchParams;
1161
1298
  const { id } = JSON.parse(body);
1162
1299
  if (!id) { res.writeHead(400); res.end(JSON.stringify({ error: 'id required' })); return; }
1163
- const loopsDir = path.join(stopQs.get('dir') || projectDir || process.cwd(), '.monomind', 'loops');
1300
+ const _stopQs = new URL(req.url, 'http://localhost').searchParams;
1301
+ const _stopDir = path.resolve(_stopQs.get('dir') || projectDir || process.cwd());
1302
+ const loopsDir = path.join(_stopDir, '.monomind', 'loops');
1164
1303
  fs.mkdirSync(loopsDir, { recursive: true });
1165
1304
  fs.writeFileSync(path.join(loopsDir, `${id}.stop`), `stop-requested-${Date.now()}`);
1166
1305
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
@@ -1173,17 +1312,27 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1173
1312
  // ---------------------------------------------------------- POST /api/loops/create
1174
1313
  if (req.method === 'POST' && url === '/api/loops/create') {
1175
1314
  let body = '';
1176
- req.on('data', chunk => { body += chunk; });
1315
+ req.on('data', chunk => { body += chunk; if (body.length > 2097152) { req.destroy(); return; } });
1177
1316
  req.on('end', () => {
1178
1317
  try {
1179
- const createQs = new URL(req.url, 'http://localhost').searchParams;
1180
- const { name, prompt, interval, maxReps } = JSON.parse(body);
1318
+ const _qs = new URL(req.url, 'http://localhost').searchParams;
1319
+ const { name: _rawName, prompt: _rawPrompt, interval: _rawInterval, maxReps: _rawMaxReps } = JSON.parse(body);
1320
+ // Cap field sizes to prevent individual large-field disk inflation.
1321
+ // The 2MB body cap already limits total payload, but a single field
1322
+ // near 2MB would produce a multi-MB loop config file per request.
1323
+ const MAX_LOOP_PROMPT_LEN = 64 * 1024; // 64 KB
1324
+ const MAX_LOOP_NAME_LEN = 512;
1325
+ const MAX_LOOP_INTERVAL_LEN = 64;
1326
+ const prompt = typeof _rawPrompt === 'string' ? _rawPrompt.slice(0, MAX_LOOP_PROMPT_LEN) : null;
1327
+ const name = typeof _rawName === 'string' ? _rawName.slice(0, MAX_LOOP_NAME_LEN) : null;
1328
+ const interval = typeof _rawInterval === 'string' ? _rawInterval.slice(0, MAX_LOOP_INTERVAL_LEN) : null;
1329
+ const maxReps = typeof _rawMaxReps === 'number' && Number.isFinite(_rawMaxReps) ? Math.max(1, Math.min(Math.floor(_rawMaxReps), 10000)) : null;
1181
1330
  if (!prompt) { res.writeHead(400); res.end(JSON.stringify({ error: 'prompt required' })); return; }
1182
- const loopsDir = path.join(createQs.get('dir') || projectDir || process.cwd(), '.monomind', 'loops');
1331
+ const loopsDir = path.join(path.resolve(_qs.get('dir') || projectDir || process.cwd()), '.monomind', 'loops');
1183
1332
  fs.mkdirSync(loopsDir, { recursive: true });
1184
1333
  const id = `loop-${Date.now()}-${Math.random().toString(36).slice(2,7)}`;
1185
1334
  const nowMs = Date.now();
1186
- const loop = { id, type: 'repeat', name: name || prompt.slice(0, 40), prompt, interval: interval || '1h', maxReps: maxReps || null, status: 'active', currentRep: 0, startedAt: nowMs, lastRunAt: null };
1335
+ const loop = { id, type: 'repeat', name: name || prompt.slice(0, 40), prompt, interval: interval || '1h', maxReps, status: 'active', currentRep: 0, startedAt: nowMs, lastRunAt: null };
1187
1336
  fs.writeFileSync(path.join(loopsDir, `${id}.json`), JSON.stringify(loop, null, 2));
1188
1337
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1189
1338
  res.end(JSON.stringify({ ok: true, id }));
@@ -1196,7 +1345,10 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1196
1345
  if (req.method === 'GET' && url === '/api/session-errors') {
1197
1346
  const qs = new URL(req.url, 'http://localhost').searchParams;
1198
1347
  const d = path.resolve(qs.get('dir') || projectDir || process.cwd());
1199
- const sessionId = qs.get('id') || '';
1348
+ // Cap sessionId to prevent O(n×m) DoS via f.includes(sessionId) substring
1349
+ // match against every filename when sessionId is a very long string.
1350
+ const _rawSessId = qs.get('id') || '';
1351
+ const sessionId = _rawSessId.slice(0, 256);
1200
1352
  const slug = d.replace(/\//g, '-');
1201
1353
  const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
1202
1354
  try {
@@ -1267,7 +1419,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1267
1419
  // ------------------------------------------------------- DELETE /api/knowledge-chunk
1268
1420
  if (req.method === 'DELETE' && url === '/api/knowledge-chunk') {
1269
1421
  let body = '';
1270
- req.on('data', chunk => { body += chunk; });
1422
+ req.on('data', chunk => { body += chunk; if (body.length > 2097152) { req.destroy(); return; } });
1271
1423
  req.on('end', () => {
1272
1424
  try {
1273
1425
  const qs = new URL(req.url, 'http://localhost').searchParams;
@@ -1306,7 +1458,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1306
1458
  // ------------------------------------------------------- PUT /api/knowledge-chunk
1307
1459
  if (req.method === 'PUT' && url === '/api/knowledge-chunk') {
1308
1460
  let body = '';
1309
- req.on('data', chunk => { body += chunk; });
1461
+ req.on('data', chunk => { body += chunk; if (body.length > 2097152) { req.destroy(); return; } });
1310
1462
  req.on('end', () => {
1311
1463
  try {
1312
1464
  const qs = new URL(req.url, 'http://localhost').searchParams;
@@ -1802,7 +1954,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1802
1954
  try {
1803
1955
  const qs = new URL(req.url, 'http://localhost').searchParams;
1804
1956
  const dir = qs.get('dir') || projectDir || process.cwd();
1805
- const q = (qs.get('q') || '').trim();
1957
+ // Cap ?q= to prevent DoS via megabyte FTS query strings.
1958
+ const q = (qs.get('q') || '').trim().slice(0, 4096);
1806
1959
  const limit = Math.min(100, parseInt(qs.get('limit') || '50', 10));
1807
1960
  const d = path.resolve(dir || process.cwd());
1808
1961
  const dbPath = path.join(d, '.monomind', 'monograph.db');
@@ -1936,7 +2089,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1936
2089
  try {
1937
2090
  const qs = new URL(req.url, 'http://localhost').searchParams;
1938
2091
  const dir = qs.get('dir') || projectDir || process.cwd();
1939
- const q = qs.get('q') || '';
2092
+ const q = (qs.get('q') || '').trim().slice(0, 4096);
1940
2093
  const d = path.resolve(dir || process.cwd());
1941
2094
  const dbPath = path.join(d, '.monomind', 'monograph.db');
1942
2095
  if (!q) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?q= parameter' })); return; }
@@ -1973,7 +2126,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1973
2126
  try {
1974
2127
  const qs = new URL(req.url, 'http://localhost').searchParams;
1975
2128
  const dir = qs.get('dir') || projectDir || process.cwd();
1976
- const nodeQ = qs.get('node') || '';
2129
+ const nodeQ = (qs.get('node') || '').trim().slice(0, 4096);
1977
2130
  const d = path.resolve(dir || process.cwd());
1978
2131
  const dbPath = path.join(d, '.monomind', 'monograph.db');
1979
2132
  if (!nodeQ) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?node= parameter' })); return; }
@@ -2015,8 +2168,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2015
2168
  try {
2016
2169
  const qs = new URL(req.url, 'http://localhost').searchParams;
2017
2170
  const dir = qs.get('dir') || projectDir || process.cwd();
2018
- const from = qs.get('from') || '';
2019
- const to = qs.get('to') || '';
2171
+ const from = (qs.get('from') || '').trim().slice(0, 4096);
2172
+ const to = (qs.get('to') || '').trim().slice(0, 4096);
2020
2173
  const d = path.resolve(dir || process.cwd());
2021
2174
  const dbPath = path.join(d, '.monomind', 'monograph.db');
2022
2175
  if (!from || !to) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?from= and ?to= parameters' })); return; }
@@ -2136,7 +2289,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2136
2289
  // -------------------------------------------------- POST /api/mcp/call
2137
2290
  if (req.method === 'POST' && url === '/api/mcp/call') {
2138
2291
  let body = '';
2139
- req.on('data', c => body += c);
2292
+ req.on('data', c => { body += c; if (body.length > 2097152) { req.destroy(); return; } });
2140
2293
  req.on('end', async () => {
2141
2294
  const json = res => { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); };
2142
2295
  const ok = (data) => { json(res); res.end(JSON.stringify({ content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }] })); };
@@ -2184,7 +2337,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2184
2337
  ok(`nodes: ${n}\nedges: ${e}`);
2185
2338
  } else if (tool === 'monograph_cypher') {
2186
2339
  // Translate basic MATCH (n:Label) queries to SQL
2187
- const q = (input.query || '').trim();
2340
+ const q = (String(input.query || '')).trim().slice(0, 4096);
2188
2341
  const labelMatch = q.match(/MATCH\s+\(n:(\w+)\)/i);
2189
2342
  if (labelMatch) {
2190
2343
  const label = labelMatch[1];
@@ -2242,12 +2395,13 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2242
2395
  } else if (tool === 'monograph_diff') {
2243
2396
  ok('Graph diff: compare two snapshots using monograph snapshot + monograph diff commands');
2244
2397
  } else if (tool === 'monograph_rename') {
2245
- const sym = input.symbolName || '';
2398
+ // Cap sym to prevent O(n) FTS scan DoS via oversized query string.
2399
+ const sym = String(input.symbolName || '').slice(0, 4096);
2246
2400
  if (!sym) { ok('Provide symbolName to rename'); return; }
2247
2401
  const hits = ftsSearch(db2, sym, 20);
2248
2402
  ok(`Found ${hits.length} occurrences of "${sym}":\n` + hits.map(h => ` ${h.filePath || '?'}:${h.startLine || '?'} — ${h.name}`).join('\n'));
2249
2403
  } else if (tool === 'monograph_impact') {
2250
- const target = input.target || '';
2404
+ const target = String(input.target || '').slice(0, 4096);
2251
2405
  const dir3 = input.direction || 'both';
2252
2406
  const depth = input.maxDepth || 4;
2253
2407
  const hits = ftsSearch(db2, target, 5);
@@ -2274,7 +2428,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2274
2428
  }
2275
2429
  ok(`Impact of "${hits[0].name}" (${dir3}, depth=${depth}):\n` + (results.join('\n') || ' (no dependencies found)'));
2276
2430
  } else if (tool === 'monograph_context') {
2277
- const id = input.id || '';
2431
+ const id = String(input.id || '').slice(0, 4096);
2278
2432
  const hits = ftsSearch(db2, id, 5);
2279
2433
  if (!hits.length) { ok(`Node not found: ${id}`); return; }
2280
2434
  const node = hits[0];
@@ -2282,7 +2436,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2282
2436
  const inEdges = db2.prepare('SELECT e.relation, n.name FROM edges e JOIN nodes n ON n.id = e.source_id WHERE e.target_id = ? LIMIT 20').all(node.id);
2283
2437
  ok(`# ${node.name} (${node.label})\nFile: ${node.filePath || '?'}\n\n**Imports / depends on (${outEdges.length}):**\n${outEdges.map(e => ` → ${e.name} [${e.relation}]`).join('\n') || ' (none)'}\n\n**Used by / depended on by (${inEdges.length}):**\n${inEdges.map(e => ` ← ${e.name} [${e.relation}]`).join('\n') || ' (none)'}`);
2284
2438
  } else if (tool === 'monograph_query' || tool === 'monograph_suggest') {
2285
- const q2 = input.query || input.task || '';
2439
+ const q2 = String(input.query || input.task || '').slice(0, 4096);
2286
2440
  const hits2 = ftsSearch(db2, q2, 20);
2287
2441
  ok(hits2.map(h => `${h.name} (${h.label}) — ${h.filePath || '?'}:${h.startLine || '?'}`).join('\n') || 'No results');
2288
2442
 
@@ -2631,7 +2785,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2631
2785
  if (['Read','Write','Edit','MultiEdit','Glob','Grep','LS'].includes(name)) return 'file';
2632
2786
  if (name === 'Bash') return 'bash';
2633
2787
  if (['Agent','Task'].includes(name)) return 'agent';
2634
- if (name.startsWith('mcp__monobrain__memory') || name.startsWith('mcp__monobrain__agentdb')) return 'memory';
2788
+ if (name.startsWith('mcp__monomind__memory') || name.startsWith('mcp__monomind__agentdb')) return 'memory';
2635
2789
  if (['WebFetch','WebSearch'].includes(name)) return 'web';
2636
2790
  if (name === 'Skill') return 'skill';
2637
2791
  return 'other';
@@ -2648,9 +2802,35 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2648
2802
  try { stat = fs.statSync(fp); } catch { continue; }
2649
2803
 
2650
2804
  // Skip files over size cap to avoid memory spikes on large sessions
2805
+ // But still do a lightweight scan for agent spawns (tool_use blocks named Agent/Task)
2651
2806
  if (stat.size > JSONL_SIZE_CAP) {
2807
+ const truncSpawns = {};
2808
+ try {
2809
+ const raw = fs.readFileSync(fp, 'utf8');
2810
+ for (const line of raw.split('\n')) {
2811
+ if (!line.includes('"tool_use"') || (!line.includes('"Agent"') && !line.includes('"Task"'))) continue;
2812
+ let e; try { e = JSON.parse(line); } catch { continue; }
2813
+ if (e.type !== 'assistant') continue;
2814
+ for (const block of (e.message?.content || [])) {
2815
+ if (!block || block.type !== 'tool_use') continue;
2816
+ if (block.name !== 'Agent' && block.name !== 'Task') continue;
2817
+ const sub = block.input?.subagent_type || block.input?.description || '?';
2818
+ truncSpawns[sub] = (truncSpawns[sub] || 0) + 1;
2819
+ }
2820
+ }
2821
+ } catch {}
2652
2822
  nodes.push({ id: sid, type: 'session', label: sid.slice(0,8), turns: 0, totalTools: 0,
2653
- toolCounts: {}, cost: 0, mtime: stat.mtimeMs, size: stat.size, agentSpawns: {}, truncated: true });
2823
+ toolCounts: {}, cost: 0, mtime: stat.mtimeMs, size: stat.size, agentSpawns: truncSpawns, truncated: true });
2824
+ for (const [subType, count] of Object.entries(truncSpawns)) {
2825
+ const nodeId = 'agent::' + subType;
2826
+ if (!agentTypeNodes[subType]) {
2827
+ agentTypeNodes[subType] = true;
2828
+ nodes.push({ id: nodeId, type: 'agenttype', label: subType, totalSpawns: 0 });
2829
+ }
2830
+ const aNode = nodes.find(n => n.id === nodeId);
2831
+ if (aNode) aNode.totalSpawns = (aNode.totalSpawns || 0) + count;
2832
+ edges.push({ source: sid, target: nodeId, weight: count, label: String(count) });
2833
+ }
2654
2834
  continue;
2655
2835
  }
2656
2836
 
@@ -2663,7 +2843,12 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2663
2843
  const lines = raw.split('\n').filter(Boolean);
2664
2844
  for (const line of lines) {
2665
2845
  let e; try { e = JSON.parse(line); } catch { continue; }
2666
- if (e.type === 'user') turns++;
2846
+ if (e.type === 'user') {
2847
+ // Only count actual human turns, not tool-result responses
2848
+ const ct = e.message?.content;
2849
+ const isToolResult = Array.isArray(ct) && ct.length > 0 && ct.every(b => b && b.type === 'tool_result');
2850
+ if (!isToolResult) turns++;
2851
+ }
2667
2852
  if (e.type === 'assistant') {
2668
2853
  for (const block of (e.message?.content || [])) {
2669
2854
  if (!block || block.type !== 'tool_use') continue;
@@ -2674,8 +2859,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2674
2859
  agentSpawns[sub] = (agentSpawns[sub] || 0) + 1;
2675
2860
  }
2676
2861
  }
2862
+ if (e.message?.usage) totalCost += _sjCalcCost(e.message.model || '', e.message.usage);
2677
2863
  }
2678
- if (e.costUSD) totalCost += e.costUSD;
2679
2864
  }
2680
2865
  } catch {}
2681
2866
 
@@ -2733,8 +2918,13 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2733
2918
  try {
2734
2919
  const qs = new URL(req.url, 'http://localhost').searchParams;
2735
2920
  const dir = qs.get('dir') || projectDir || process.cwd();
2736
- const swarmId = qs.get('swarmId') || undefined;
2737
- const agentId = qs.get('agentId') || undefined;
2921
+ // Cap swarmId and agentId to prevent O(n×m) DoS: filter() compares
2922
+ // each event against the query string, so a megabyte-scale ID causes
2923
+ // O(events × m) string comparisons.
2924
+ const _rawSwarmId = qs.get('swarmId') || undefined;
2925
+ const _rawAgentId = qs.get('agentId') || undefined;
2926
+ const swarmId = typeof _rawSwarmId === 'string' ? _rawSwarmId.slice(0, 256) : undefined;
2927
+ const agentId = typeof _rawAgentId === 'string' ? _rawAgentId.slice(0, 256) : undefined;
2738
2928
  const last = qs.get('last') ? parseInt(qs.get('last')) : undefined;
2739
2929
  const events = collectSwarmEvents(path.resolve(dir), { swarmId, agentId, last });
2740
2930
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
@@ -2778,47 +2968,71 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2778
2968
  if (req.method === 'GET' && url.startsWith('/api/token-usage')) {
2779
2969
  try {
2780
2970
  const qs = new URL(req.url, 'http://localhost').searchParams;
2781
- const period = qs.get('period') || 'today';
2971
+ const period = ['today','week','30days','month'].includes(qs.get('period')) ? qs.get('period') : 'today';
2782
2972
  const dir = path.resolve(qs.get('dir') || projectDir || process.cwd());
2783
-
2784
- // Determine date window
2785
- const now = new Date();
2786
- const todayStr = now.toISOString().slice(0, 10);
2787
- let fromStr;
2788
- let daysBack;
2789
- if (period === 'today') {
2790
- fromStr = todayStr; daysBack = 1;
2791
- } else if (period === 'week') {
2792
- const d = new Date(now); d.setDate(d.getDate() - 6);
2793
- fromStr = d.toISOString().slice(0, 10); daysBack = 7;
2794
- } else if (period === '30d' || period === '30days') {
2795
- const d = new Date(now); d.setDate(d.getDate() - 29);
2796
- fromStr = d.toISOString().slice(0, 10); daysBack = 30;
2797
- } else { // month
2798
- fromStr = now.toISOString().slice(0, 7) + '-01'; daysBack = 32;
2799
- }
2800
-
2801
- const raw = collectTokens(dir, Math.max(daysBack + 1, 14));
2802
- // Filter daily to the requested window
2803
- const daily = (raw.daily || []).filter(d => d.date >= fromStr);
2804
- // Aggregate summary for the period
2805
- let cost = 0, calls = 0, tokensIn = 0, tokensOut = 0;
2806
- for (const d of daily) { cost += d.cost; calls += d.calls; tokensIn += d.tokensIn; tokensOut += d.tokensOut; }
2807
- // Filter rows to sessions that had activity in the window
2808
- const rows = (raw.rows || []).filter(r => {
2809
- // rows don't have dates yet, just include top ones by cost
2810
- return true;
2811
- }).slice(0, 30);
2812
-
2813
- const summary = {
2814
- todayCost: cost,
2815
- todayCalls: calls,
2816
- cost, calls,
2817
- totalTokens: tokensIn + tokensOut,
2818
- modelCount: Object.keys(raw.summary?.modelBreakdown || {}).length || null,
2973
+ const trackerPath = path.join(dir, '.claude', 'helpers', 'token-tracker.cjs');
2974
+ const fallback = () => {
2975
+ const summary = (() => { try { return JSON.parse(fs.readFileSync(path.join(dir, '.monomind', 'metrics', 'token-summary.json'), 'utf8')); } catch { return {}; } })();
2976
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
2977
+ const fbSum = { todayCost: summary.todayCost || 0, cost: summary.todayCost || 0, todayCalls: summary.todayCalls || 0, calls: summary.todayCalls || 0, totalTokens: 0, totalTokensIn: 0, totalTokensOut: 0, cacheTokens: 0, modelCount: 0 };
2978
+ res.end(JSON.stringify({ summary: fbSum, totalCost: summary.todayCost || 0, totalCalls: summary.todayCalls || 0, totalIn: 0, totalOut: 0, totalCR: 0, totalCW: 0, rows: [], models: [], categories: [], tools: [], mcpServers: [], projects: [], modelBreakdown: {}, categoryBreakdown: {}, toolBreakdown: {}, mcpBreakdown: {}, periodLabel: period }));
2819
2979
  };
2820
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
2821
- res.end(JSON.stringify({ summary, daily, rows, periodLabel: period }));
2980
+ if (!fs.existsSync(trackerPath)) { fallback(); return; }
2981
+ try {
2982
+ const _req = createRequire(import.meta.url);
2983
+ const tracker = _req(trackerPath);
2984
+ const range = tracker.getDateRange(period);
2985
+ const projects = tracker.parseAllSessions(range.start, range.end);
2986
+ let totalCost = 0, totalIn = 0, totalOut = 0, totalCR = 0, totalCW = 0, totalCalls = 0;
2987
+ const modelBreakdown = {}, categoryBreakdown = {}, toolBreakdown = {}, mcpBreakdown = {};
2988
+ for (const p of projects) {
2989
+ totalCost += p.totalCost || 0;
2990
+ for (const s of (p.sessions || [])) {
2991
+ totalIn += s.totalInputTokens || 0;
2992
+ totalOut += s.totalOutputTokens || 0;
2993
+ totalCR += s.totalCacheRead || 0;
2994
+ totalCW += s.totalCacheWrite || 0;
2995
+ totalCalls += s.apiCalls || 0;
2996
+ for (const [mn, m] of Object.entries(s.modelBreakdown || {})) {
2997
+ if (!modelBreakdown[mn]) modelBreakdown[mn] = { calls: 0, cost: 0, tokens: 0 };
2998
+ modelBreakdown[mn].calls += m.calls || 0;
2999
+ modelBreakdown[mn].cost += m.cost || 0;
3000
+ modelBreakdown[mn].tokens += m.tokens || 0;
3001
+ }
3002
+ for (const [cat, c] of Object.entries(s.categoryBreakdown || {})) {
3003
+ if (!categoryBreakdown[cat]) categoryBreakdown[cat] = { turns: 0, cost: 0 };
3004
+ categoryBreakdown[cat].turns += c.turns || 0;
3005
+ categoryBreakdown[cat].cost += c.cost || 0;
3006
+ }
3007
+ for (const [tool, t] of Object.entries(s.toolBreakdown || {})) {
3008
+ if (!toolBreakdown[tool]) toolBreakdown[tool] = { calls: 0 };
3009
+ toolBreakdown[tool].calls += t.calls || 0;
3010
+ }
3011
+ for (const [srv, m] of Object.entries(s.mcpBreakdown || {})) {
3012
+ if (!mcpBreakdown[srv]) mcpBreakdown[srv] = { calls: 0 };
3013
+ mcpBreakdown[srv].calls += m.calls || 0;
3014
+ }
3015
+ }
3016
+ }
3017
+ // Build client-friendly arrays from breakdown dicts
3018
+ const models = Object.entries(modelBreakdown).map(([model, m]) => ({ model, cost: m.cost, calls: m.calls, tokens: m.tokens })).sort((a, b) => b.cost - a.cost);
3019
+ const categories = Object.entries(categoryBreakdown).map(([category, c]) => ({ category, turns: c.turns, cost: c.cost })).sort((a, b) => b.turns - a.turns);
3020
+ const tools = Object.entries(toolBreakdown).map(([tool, t]) => ({ tool, count: t.calls })).sort((a, b) => b.count - a.count);
3021
+ const mcpServers = Object.entries(mcpBreakdown).map(([server, m]) => ({ server, count: m.calls })).sort((a, b) => b.count - a.count);
3022
+ const projectRows = projects.map(p => ({ project: p.name || p.slug || p.dir || '?', cost: p.totalCost || 0 })).sort((a, b) => b.cost - a.cost);
3023
+ // Build rows array from sessions for per-session table
3024
+ const rows = [];
3025
+ for (const p of projects) {
3026
+ for (const s of (p.sessions || [])) {
3027
+ rows.push({ id: s.id || '', session: s.lastPrompt || s.id || '', calls: s.apiCalls || 0, cost: s.totalCost || 0, tokens: (s.totalInputTokens || 0) + (s.totalOutputTokens || 0) });
3028
+ }
3029
+ }
3030
+ rows.sort((a, b) => b.cost - a.cost);
3031
+ // Summary object matching client expectations
3032
+ const summary = { todayCost: totalCost, cost: totalCost, todayCalls: totalCalls, calls: totalCalls, totalTokens: totalIn + totalOut, totalTokensIn: totalIn, totalTokensOut: totalOut, cacheTokens: totalCR, modelCount: models.length };
3033
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
3034
+ res.end(JSON.stringify({ summary, totalCost, totalCalls, totalIn, totalOut, totalCR, totalCW, rows, models, categories, tools, mcpServers, projects: projectRows, modelBreakdown, categoryBreakdown, toolBreakdown, mcpBreakdown, periodLabel: period }));
3035
+ } catch (e) { fallback(); }
2822
3036
  } catch (err) {
2823
3037
  res.writeHead(500, { 'Content-Type': 'application/json' });
2824
3038
  res.end(JSON.stringify({ error: err.message }));
@@ -2972,7 +3186,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2972
3186
  const orgsDir = path.join(path.resolve(_orgsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
2973
3187
  let orgs = [];
2974
3188
  if (fs.existsSync(orgsDir)) {
2975
- const _sidecarSuffixRe = /-(approvals|state|activity|goals|routines|projects|members|issues|workspaces|worktrees|environments|plugins|adapters|bootstrap|threads|budgets|project-workspaces|approval-comments|secrets)\.json$/;
3189
+ const _sidecarSuffixRe = /-(approvals|state|activity|goals|routines|projects|members|issues|workspaces|worktrees|environments|plugins|adapters|bootstrap|threads|budgets|project-workspaces|approval-comments|secrets|join-requests|skills)\.json$/;
2976
3190
  const files = fs.readdirSync(orgsDir).filter(f => f.endsWith('.json') && !_sidecarSuffixRe.test(f));
2977
3191
  // Read events file once, outside the per-org loop
2978
3192
  let recentLines = [];
@@ -3003,7 +3217,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3003
3217
  const stopTs = lastStop ? (JSON.parse(lastStop).ts || 0) : 0;
3004
3218
  running = startTs > stopTs;
3005
3219
  }
3006
- orgs.push({ name: cfg.name, goal: cfg.goal, roles: cfg.roles?.length || 0, topology: cfg.topology, created_at: cfg.created_at, running, status: cfg.status, loop: cfg.loop ? { poll_interval_minutes: cfg.loop.poll_interval_minutes, last_run: cfg.loop.last_run, next_run: cfg.loop.next_run } : undefined });
3220
+ orgs.push({ name: cfg.name, goal: cfg.goal, roles: Array.isArray(cfg.roles) ? cfg.roles : [], topology: cfg.topology, created_at: cfg.created_at, running, status: cfg.status, loop: cfg.loop ? { poll_interval_minutes: cfg.loop.poll_interval_minutes, last_run: cfg.loop.last_run, next_run: cfg.loop.next_run } : undefined });
3007
3221
  } catch(_) {}
3008
3222
  }
3009
3223
  }
@@ -3013,12 +3227,65 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3013
3227
  return;
3014
3228
  }
3015
3229
 
3230
+ // POST /api/orgs/:name/import — import an org config by name (orgs.html upload flow)
3231
+ if (req.method === 'POST' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/import$/i.test(url)) {
3232
+ let body = '';
3233
+ req.on('data', c => { body += c; if (body.length > 2e6) req.destroy(); });
3234
+ req.on('end', () => {
3235
+ try {
3236
+ const urlParts = url.split('/');
3237
+ const orgName = decodeURIComponent(urlParts[3]);
3238
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid org name' })); return; }
3239
+ const cfg = JSON.parse(body);
3240
+ const _importQs = new URL(req.url, 'http://localhost').searchParams;
3241
+ const dir = path.resolve(_importQs.get('dir') || projectDir || process.cwd());
3242
+ const orgsDir = path.join(dir, '.monomind', 'orgs');
3243
+ fs.mkdirSync(orgsDir, { recursive: true });
3244
+ const destFile = path.join(orgsDir, `${orgName}.json`);
3245
+ fs.writeFileSync(destFile, JSON.stringify({ ...cfg, name: orgName }, null, 2), 'utf8');
3246
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3247
+ res.end(JSON.stringify({ ok: true, name: orgName, file: destFile }));
3248
+ } catch (e) {
3249
+ res.writeHead(400, { 'Content-Type': 'application/json' });
3250
+ res.end(JSON.stringify({ error: e.message }));
3251
+ }
3252
+ });
3253
+ return;
3254
+ }
3255
+
3256
+ // POST /api/orgs — import / create org from JSON body
3257
+ if (req.method === 'POST' && url === '/api/orgs') {
3258
+ let body = '';
3259
+ req.on('data', c => { body += c; if (body.length > 2e6) req.destroy(); });
3260
+ req.on('end', () => {
3261
+ try {
3262
+ const cfg = JSON.parse(body);
3263
+ const qs = new URL(req.url, 'http://localhost').searchParams;
3264
+ const dir = qs.get('dir') || cfg.dir || projectDir || process.cwd();
3265
+ const name = (cfg.name || '').toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
3266
+ if (!name) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid org name' })); return; }
3267
+ const orgsDir = path.join(path.resolve(dir), '.monomind', 'orgs');
3268
+ fs.mkdirSync(orgsDir, { recursive: true });
3269
+ const destFile = path.join(orgsDir, `${name}.json`);
3270
+ const cleanCfg = Object.fromEntries(Object.entries({ ...cfg, name }).filter(([k]) => !k.startsWith('_')));
3271
+ fs.writeFileSync(destFile, JSON.stringify(cleanCfg, null, 2), 'utf8');
3272
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3273
+ res.end(JSON.stringify({ ok: true, name, file: destFile }));
3274
+ } catch (e) {
3275
+ res.writeHead(400, { 'Content-Type': 'application/json' });
3276
+ res.end(JSON.stringify({ error: e.message }));
3277
+ }
3278
+ });
3279
+ return;
3280
+ }
3281
+
3016
3282
  // GET /api/orgs/:name — get specific org config (exact path: /api/orgs/<slug>)
3017
3283
  if (req.method === 'GET' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)) {
3018
3284
  try {
3019
3285
  const orgName = decodeURIComponent(url.slice('/api/orgs/'.length));
3020
3286
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3021
- const f = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}.json`);
3287
+ const _orgsOneQs = new URL(req.url, 'http://localhost').searchParams;
3288
+ const f = path.join(path.resolve(_orgsOneQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs', `${orgName}.json`);
3022
3289
  if (!fs.existsSync(f)) { res.writeHead(404); res.end('{"error":"not found"}'); return; }
3023
3290
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3024
3291
  res.end(fs.readFileSync(f, 'utf8'));
@@ -3050,8 +3317,17 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3050
3317
  const stopFile = path.join(orgsDir, '.stops', `${orgName}.stop`);
3051
3318
  const running = !fs.existsSync(stopFile) && Object.values(state.agents || {}).some(a => a.status === 'running');
3052
3319
 
3320
+ // Read real tasks from the task store and group by status column
3321
+ const taskStoreData = readJsonSafe(path.join(d, '.monomind', 'tasks', 'store.json'));
3322
+ const allTasks = taskStoreData ? Object.values(taskStoreData.tasks || {}) : [];
3323
+ const tasks = {
3324
+ todo: allTasks.filter(t => t.status === 'pending').map(t => ({ id: t.taskId, description: t.description, status: 'todo', ts: t.createdAt })),
3325
+ doing: allTasks.filter(t => t.status === 'in_progress').map(t => ({ id: t.taskId, description: t.description, status: 'doing', ts: t.startedAt || t.createdAt })),
3326
+ done: allTasks.filter(t => t.status === 'completed' || t.status === 'failed' || t.status === 'cancelled').map(t => ({ id: t.taskId, description: t.description, status: t.status, ts: t.completedAt || t.createdAt })),
3327
+ };
3328
+
3053
3329
  const result = { config, state, goals: goalsData.goals, routines: routinesData.routines,
3054
- approvals: approvalsData.approvals, running, tasks: { todo: [], doing: [], done: [] } };
3330
+ approvals: approvalsData.approvals, running, tasks };
3055
3331
 
3056
3332
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3057
3333
  res.end(JSON.stringify(result));
@@ -3115,7 +3391,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3115
3391
  const parts = url.split('/');
3116
3392
  const orgName = decodeURIComponent(parts[3]);
3117
3393
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('[]'); return; }
3118
- const d = projectDir || process.cwd();
3394
+ const _projsQs = new URL(req.url, 'http://localhost').searchParams;
3395
+ const d = path.resolve(_projsQs.get('dir') || projectDir || process.cwd());
3119
3396
  const projFile = path.join(d, '.monomind', 'orgs', `${orgName}-projects.json`);
3120
3397
  if (!fs.existsSync(projFile)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end('[]'); return; }
3121
3398
  const data = JSON.parse(fs.readFileSync(projFile, 'utf8'));
@@ -3131,7 +3408,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3131
3408
  const parts = url.split('/');
3132
3409
  const orgName = decodeURIComponent(parts[3]);
3133
3410
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
3134
- const d = projectDir || process.cwd();
3411
+ const _membersQs = new URL(req.url, 'http://localhost').searchParams;
3412
+ const d = path.resolve(_membersQs.get('dir') || projectDir || process.cwd());
3135
3413
  const membersFile = path.join(d, '.monomind', 'orgs', `${orgName}-members.json`);
3136
3414
  if (!fs.existsSync(membersFile)) {
3137
3415
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
@@ -3151,7 +3429,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3151
3429
  const parts = url.split('/');
3152
3430
  const orgName = decodeURIComponent(parts[3]);
3153
3431
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
3154
- const d = projectDir || process.cwd();
3432
+ const _adaptersQs = new URL(req.url, 'http://localhost').searchParams;
3433
+ const d = path.resolve(_adaptersQs.get('dir') || projectDir || process.cwd());
3155
3434
  const adaptersFile = path.join(d, '.monomind', 'orgs', `${orgName}-adapters.json`);
3156
3435
  if (!fs.existsSync(adaptersFile)) {
3157
3436
  // Return defaults derived from org config if available
@@ -3179,7 +3458,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3179
3458
  const parts = url.split('/');
3180
3459
  const orgName = decodeURIComponent(parts[3]);
3181
3460
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
3182
- const d = projectDir || process.cwd();
3461
+ const _skillsQs = new URL(req.url, 'http://localhost').searchParams;
3462
+ const d = path.resolve(_skillsQs.get('dir') || projectDir || process.cwd());
3183
3463
  const skillsDir = path.join(d, '.claude', 'skills');
3184
3464
  const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
3185
3465
 
@@ -3286,13 +3566,13 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3286
3566
  // GET /api/org/:name/search?q=<query> — fuzzy search across org data
3287
3567
  if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/search(\?.*)?$/i.test(url)) {
3288
3568
  try {
3289
- const urlObj = new URL(`http://x${url}`);
3569
+ const urlObj = new URL(`http://x${req.url}`);
3290
3570
  const orgName = decodeURIComponent(urlObj.pathname.split('/')[3]);
3291
3571
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
3292
3572
  const q = (urlObj.searchParams.get('q') || '').toLowerCase().trim();
3293
3573
  if (!q || q.length < 2) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end('{"hits":[]}'); return; }
3294
3574
 
3295
- const d = projectDir || process.cwd();
3575
+ const d = path.resolve(urlObj.searchParams.get('dir') || projectDir || process.cwd());
3296
3576
  const orgsDir = path.join(d, '.monomind', 'orgs');
3297
3577
  const readJ = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
3298
3578
 
@@ -3310,8 +3590,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3310
3590
  // Goals
3311
3591
  const goals = readJ(path.join(orgsDir, `${orgName}-goals.json`));
3312
3592
  for (const g of (goals?.goals || [])) {
3313
- if (match(g.title) || match(g.description)) {
3314
- hits.push({ type: 'goal', id: g.id, title: g.title, meta: g.status || 'open' });
3593
+ if (match(g.title) || match(g.text) || match(g.goal) || match(g.description)) {
3594
+ hits.push({ type: 'goal', id: g.id, title: g.title || g.text || g.goal, meta: g.status || 'open' });
3315
3595
  }
3316
3596
  }
3317
3597
 
@@ -3339,6 +3619,14 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3339
3619
  }
3340
3620
  }
3341
3621
 
3622
+ // Issues
3623
+ const issuesData = readJ(path.join(orgsDir, `${orgName}-issues.json`));
3624
+ for (const i of (issuesData?.issues || [])) {
3625
+ if (match(i.title) || match(i.description) || match(i.slug)) {
3626
+ hits.push({ type: 'issue', id: i.id || i.slug, title: i.title || i.slug, meta: i.status || 'open' });
3627
+ }
3628
+ }
3629
+
3342
3630
  // Recent activity events
3343
3631
  const eventsFile = path.join(d, 'data', 'mastermind-events.jsonl');
3344
3632
  if (fs.existsSync(eventsFile)) {
@@ -3365,13 +3653,17 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3365
3653
  try {
3366
3654
  const orgName = decodeURIComponent(url.split('/')[3]);
3367
3655
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3368
- const issuesPath = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-issues.json`);
3656
+ const _issuesQs = new URL(req.url, 'http://localhost').searchParams;
3657
+ const _issuesDir = path.resolve(_issuesQs.get('dir') || projectDir || process.cwd());
3658
+ const issuesPath = path.join(_issuesDir, '.monomind', 'orgs', `${orgName}-issues.json`);
3369
3659
  let payload = { issues: [] };
3370
3660
  try {
3371
3661
  const raw = JSON.parse(fs.readFileSync(issuesPath, 'utf8'));
3372
3662
  payload.issues = (raw.issues || []).map(i => ({
3373
- id: i.id, slug: i.slug, title: i.title, status: i.status || 'open',
3663
+ id: i.id, slug: i.slug, title: i.title, description: i.description || null,
3664
+ status: i.status || 'open',
3374
3665
  priority: i.priority || 'medium', assignee_id: i.assignee_id || null,
3666
+ assignee: i.assignee || i.assignee_id || null,
3375
3667
  project_id: i.project_id || null, parent_id: i.parent_id || null,
3376
3668
  created_at: i.created_at, updated_at: i.updated_at
3377
3669
  }));
@@ -3422,11 +3714,12 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3422
3714
  try {
3423
3715
  const actPath = path.join(base, `${orgName}-activity.jsonl`);
3424
3716
  const lines = fs.readFileSync(actPath, 'utf8').split('\n').filter(Boolean);
3425
- const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
3717
+ const cutoffMs = Date.now() - 7 * 24 * 60 * 60 * 1000;
3426
3718
  lines.forEach(line => {
3427
3719
  try {
3428
3720
  const ev = JSON.parse(line);
3429
- if (!ev.ts || ev.ts < cutoff) return;
3721
+ const evMs = typeof ev.ts === 'number' ? ev.ts : (ev.ts ? Date.parse(ev.ts) : 0);
3722
+ if (!evMs || evMs < cutoffMs) return;
3430
3723
  totalRuns++;
3431
3724
  if (ev.type && ev.type.includes('complete')) successRuns++;
3432
3725
  } catch(_) {}
@@ -3460,7 +3753,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3460
3753
  try {
3461
3754
  const orgName = decodeURIComponent(url.split('/')[3]);
3462
3755
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3463
- const envsPath = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-environments.json`);
3756
+ const _envsQs = new URL(req.url, 'http://localhost').searchParams;
3757
+ const envsPath = path.join(path.resolve(_envsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs', `${orgName}-environments.json`);
3464
3758
  let payload = { environments: [], default_env: null };
3465
3759
  try {
3466
3760
  const raw = JSON.parse(fs.readFileSync(envsPath, 'utf8'));
@@ -3486,7 +3780,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3486
3780
  try {
3487
3781
  const orgName = decodeURIComponent(url.split('/')[3]);
3488
3782
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3489
- const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
3783
+ const _wsQs = new URL(req.url, 'http://localhost').searchParams;
3784
+ const base = path.join(path.resolve(_wsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
3490
3785
  let payload = { workspaces: [] };
3491
3786
  try {
3492
3787
  const wsRaw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-workspaces.json`), 'utf8'));
@@ -3509,17 +3804,18 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3509
3804
  }
3510
3805
 
3511
3806
  // GET /api/org/:name/invites — active invites + pending join requests
3512
- if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/invites$/i)) {
3807
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/invites(\?.*)?$/i)) {
3513
3808
  try {
3514
- const orgName = decodeURIComponent(url.split('/')[3]);
3809
+ const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
3515
3810
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3516
- const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
3811
+ const _invitesQs = new URL(req.url, 'http://localhost').searchParams;
3812
+ const base = path.join(path.resolve(_invitesQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
3517
3813
  let payload = { invites: [], join_requests: [] };
3518
3814
  try {
3519
3815
  const raw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-members.json`), 'utf8'));
3520
3816
  const all = raw.join_requests || [];
3521
3817
  payload.invites = all.filter(r => r.type === 'invite' && r.status === 'pending')
3522
- .map(r => ({ id: r.id, token: r.token ? r.token.slice(0, 8) + '…' : r.id, role: r.role || 'operator', createdAt: r.createdAt || null, status: r.status }));
3818
+ .map(r => ({ id: r.id, token: r.token ? r.token.slice(0, 8) + '…' : r.id, role: r.role || 'operator', createdAt: r.createdAt || null, expiresAt: r.expiresAt || null, status: r.status }));
3523
3819
  payload.join_requests = all.filter(r => r.type !== 'invite' && r.status === 'pending_approval')
3524
3820
  .map(r => ({ id: r.id, requestType: r.requestType || 'human', role: r.role || 'viewer', createdAt: r.createdAt || null, message: r.message || '' }));
3525
3821
  } catch(_) { /* members file missing */ }
@@ -3534,7 +3830,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3534
3830
  try {
3535
3831
  const orgName = decodeURIComponent(url.split('/')[3]);
3536
3832
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3537
- const base = path.join(projectDir || process.cwd(), '.monomind');
3833
+ const _pluginsQs = new URL(req.url, 'http://localhost').searchParams;
3834
+ const base = path.join(path.resolve(_pluginsQs.get('dir') || projectDir || process.cwd()), '.monomind');
3538
3835
  let plugins = [];
3539
3836
  try {
3540
3837
  const reg = JSON.parse(fs.readFileSync(path.join(base, 'plugins', 'registry.json'), 'utf8'));
@@ -3572,7 +3869,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3572
3869
  try {
3573
3870
  const orgName = decodeURIComponent(url.split('/')[3]);
3574
3871
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3575
- const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
3872
+ const _myIssuesQs = new URL(req.url, 'http://localhost').searchParams;
3873
+ const base = path.join(path.resolve(_myIssuesQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
3576
3874
  let payload = { issues: [] };
3577
3875
  try {
3578
3876
  const raw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-issues.json`), 'utf8'));
@@ -3582,12 +3880,15 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3582
3880
  .map(i => ({
3583
3881
  id: i.id,
3584
3882
  title: i.title || null,
3883
+ description: i.description || null,
3585
3884
  status: i.status || 'open',
3586
3885
  priority: i.priority || 'medium',
3587
3886
  assigneeId: i.assigneeId || i.assigned_to || null,
3588
3887
  projectId: i.projectId || i.project_id || null,
3589
3888
  createdAt: i.createdAt || null,
3590
3889
  lastActivityAt: i.lastActivityAt || null,
3890
+ updated_at: i.updated_at || i.lastActivityAt || i.updatedAt || i.ts || null,
3891
+ ts: i.ts || i.updated_at || i.lastActivityAt || null,
3591
3892
  }));
3592
3893
  } catch(_) { /* issues file missing */ }
3593
3894
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -3601,7 +3902,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3601
3902
  try {
3602
3903
  const orgName = decodeURIComponent(url.split('/')[3]);
3603
3904
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3604
- const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
3905
+ const _agentsQs = new URL(req.url, 'http://localhost').searchParams;
3906
+ const d = path.resolve(_agentsQs.get('dir') || projectDir || process.cwd());
3907
+ const base = path.join(d, '.monomind', 'orgs');
3605
3908
  const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
3606
3909
  const config = readJsonSafe(path.join(base, `${orgName}.json`)) || {};
3607
3910
  const stateData = readJsonSafe(path.join(base, `${orgName}-state.json`)) || {};
@@ -3614,8 +3917,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3614
3917
  return {
3615
3918
  id: r.id,
3616
3919
  title: r.title || r.id,
3617
- adapterType: (r.adapter && r.adapter.type) || null,
3618
- adapterModel: (r.adapter && r.adapter.model) || null,
3920
+ adapterType: r.agent_type || r.type || null,
3921
+ adapterModel: (r.adapter_config && r.adapter_config.model) || (r.adapter && r.adapter.model) || null,
3619
3922
  governance: r.governance || null,
3620
3923
  reportsTo: r.reports_to || null,
3621
3924
  status: s.status || 'idle',
@@ -3636,7 +3939,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3636
3939
  try {
3637
3940
  const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
3638
3941
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3639
- const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
3942
+ const _approvalsQs = new URL(req.url, 'http://localhost').searchParams;
3943
+ const base = path.join(path.resolve(_approvalsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
3640
3944
  const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
3641
3945
  const data = readJsonSafe(path.join(base, `${orgName}-approvals.json`)) || { approvals: [] };
3642
3946
  const approvals = (data.approvals || [])
@@ -3671,7 +3975,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3671
3975
  // Body: { action: "approve" | "reject" | "revision_requested" }
3672
3976
  if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals\/[^/]+$/i)) {
3673
3977
  let body = '';
3674
- for await (const chunk of req) body += chunk;
3978
+ for await (const chunk of req) { body += chunk; if (body.length > 2097152) { req.destroy(); break; } }
3675
3979
  try {
3676
3980
  const parts = url.split('/');
3677
3981
  const orgName = decodeURIComponent(parts[3]);
@@ -3683,7 +3987,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3683
3987
  if (!['approve', 'reject', 'revision_requested'].includes(action)) {
3684
3988
  res.writeHead(400); res.end('{"error":"action must be approve, reject, or revision_requested"}'); return;
3685
3989
  }
3686
- const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
3990
+ const _postApprovalsQs = new URL(req.url, 'http://localhost').searchParams;
3991
+ const base = path.join(path.resolve(_postApprovalsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
3687
3992
  const approvalsFile = path.join(base, `${orgName}-approvals.json`);
3688
3993
  let data = { approvals: [] };
3689
3994
  try { data = JSON.parse(fs.readFileSync(approvalsFile, 'utf8')); } catch(_) {}
@@ -3701,7 +4006,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3701
4006
  fs.renameSync(tmp, approvalsFile);
3702
4007
  // Emit org:approval:resolved event so boss agent unblocks
3703
4008
  const event = { type: 'org:approval:resolved', org: orgName, approval_id: approvalId, status, ts: Date.now() };
3704
- try { fs.appendFileSync(path.join(projectDir || process.cwd(), 'data', 'mastermind-events.jsonl'), JSON.stringify(event) + '\n'); } catch(_) {}
4009
+ try { fs.appendFileSync(path.join(path.resolve(_postApprovalsQs.get('dir') || projectDir || process.cwd()), 'data', 'mastermind-events.jsonl'), JSON.stringify(event) + '\n'); } catch(_) {}
3705
4010
  const msg = `data: ${JSON.stringify(event)}\n\n`;
3706
4011
  for (const c of mmSseClients) { try { c.write(msg); } catch(_) { mmSseClients.delete(c); } }
3707
4012
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
@@ -3715,7 +4020,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3715
4020
  try {
3716
4021
  const orgName = decodeURIComponent(url.split('/')[3]);
3717
4022
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3718
- const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
4023
+ const _secretsQs = new URL(req.url, 'http://localhost').searchParams;
4024
+ const base = path.join(path.resolve(_secretsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
3719
4025
  const secretsDir = path.join(base, '.secrets');
3720
4026
  const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
3721
4027
  // Read secrets index — NEVER expose actual values
@@ -3723,6 +4029,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3723
4029
  const data = readJsonSafe(indexFile) || { secrets: [] };
3724
4030
  const secrets = (data.secrets || []).map(s => ({
3725
4031
  name: s.name,
4032
+ purpose: s.purpose || null,
3726
4033
  maskedRef: s.maskedRef || `${(s.name||'').substring(0,4)}***`,
3727
4034
  status: s.status || 'active',
3728
4035
  createdAt: s.createdAt || null,
@@ -3742,7 +4049,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3742
4049
  try {
3743
4050
  const orgName = decodeURIComponent(url.split('/')[3]);
3744
4051
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3745
- const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
4052
+ const _budgetsQs = new URL(req.url, 'http://localhost').searchParams;
4053
+ const base = path.join(path.resolve(_budgetsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
3746
4054
  let budgetData = { org_budget: {}, agent_budgets: {}, period: 'monthly', currency: 'USD' };
3747
4055
  try { budgetData = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-budgets.json`), 'utf8')); } catch(_) {}
3748
4056
  // Enrich with per-agent spend from state file.
@@ -3785,12 +4093,17 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3785
4093
  try {
3786
4094
  const orgName = decodeURIComponent(url.split('/')[3]);
3787
4095
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3788
- const threadsFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-threads.jsonl`);
4096
+ const _threadsQs = new URL(req.url, 'http://localhost').searchParams;
4097
+ const threadsFile = path.join(path.resolve(_threadsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs', `${orgName}-threads.jsonl`);
3789
4098
  let threads = [];
3790
4099
  try {
3791
4100
  const lines = fs.readFileSync(threadsFile, 'utf8').split('\n').filter(l => l.trim());
3792
4101
  threads = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
3793
- threads = threads.filter(t => t.type === 'thread' || !t.type);
4102
+ threads = threads.filter(t => t.type === 'thread' || !t.type).map(t => ({
4103
+ ...t,
4104
+ author: t.author || t.authorName || t.createdBy || t.authorId || null,
4105
+ messageCount: t.messageCount != null ? t.messageCount : (Array.isArray(t.messages) ? t.messages.length : (typeof t.messages === 'number' ? t.messages : null)),
4106
+ }));
3794
4107
  } catch(_) {}
3795
4108
  res.writeHead(200, { 'Content-Type': 'application/json' });
3796
4109
  res.end(JSON.stringify({ threads }));
@@ -3804,7 +4117,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3804
4117
  try {
3805
4118
  const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
3806
4119
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3807
- const joinFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-join-requests.json`);
4120
+ const _joinQs = new URL(req.url, 'http://localhost').searchParams;
4121
+ const joinFile = path.join(path.resolve(_joinQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs', `${orgName}-join-requests.json`);
3808
4122
  let requests = [];
3809
4123
  try {
3810
4124
  const raw = fs.readFileSync(joinFile, 'utf8');
@@ -3831,7 +4145,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3831
4145
  try {
3832
4146
  const orgName = decodeURIComponent(url.split('/')[3]);
3833
4147
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3834
- const goalsFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-goals.json`);
4148
+ const _goalsQs = new URL(req.url, 'http://localhost').searchParams;
4149
+ const goalsFile = path.join(path.resolve(_goalsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs', `${orgName}-goals.json`);
3835
4150
  let data = { goals: [] };
3836
4151
  try { data = JSON.parse(fs.readFileSync(goalsFile, 'utf8')); } catch(_) {}
3837
4152
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
@@ -3845,7 +4160,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3845
4160
  try {
3846
4161
  const orgName = decodeURIComponent(url.split('/')[3]);
3847
4162
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3848
- const routinesFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-routines.json`);
4163
+ const _routinesQs = new URL(req.url, 'http://localhost').searchParams;
4164
+ const routinesFile = path.join(path.resolve(_routinesQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs', `${orgName}-routines.json`);
3849
4165
  let data = { routines: [] };
3850
4166
  try { data = JSON.parse(fs.readFileSync(routinesFile, 'utf8')); } catch(_) {}
3851
4167
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
@@ -3858,13 +4174,14 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3858
4174
  // Body: { goals: [{id, title, description, status, priority, assignee_id, created_at}] }
3859
4175
  if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/goals$/i)) {
3860
4176
  let body = '';
3861
- for await (const chunk of req) body += chunk;
4177
+ for await (const chunk of req) { body += chunk; if (body.length > 2097152) { req.destroy(); break; } }
3862
4178
  try {
3863
4179
  const orgName = decodeURIComponent(url.split('/')[3]);
3864
4180
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3865
4181
  const parsed = JSON.parse(body);
3866
4182
  if (!parsed || !Array.isArray(parsed.goals)) { res.writeHead(400); res.end('{"error":"goals array required"}'); return; }
3867
- const goalsFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-goals.json`);
4183
+ const _postGoalsQs = new URL(req.url, 'http://localhost').searchParams;
4184
+ const goalsFile = path.join(path.resolve(_postGoalsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs', `${orgName}-goals.json`);
3868
4185
  const tmp = `${goalsFile}.tmp`;
3869
4186
  const payload = { org: orgName, updated_at: new Date().toISOString(), goals: parsed.goals };
3870
4187
  fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
@@ -3879,13 +4196,14 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3879
4196
  // Body: { routines: [{name, description, schedule, enabled, last_run, next_run}] }
3880
4197
  if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/routines$/i)) {
3881
4198
  let body = '';
3882
- for await (const chunk of req) body += chunk;
4199
+ for await (const chunk of req) { body += chunk; if (body.length > 2097152) { req.destroy(); break; } }
3883
4200
  try {
3884
4201
  const orgName = decodeURIComponent(url.split('/')[3]);
3885
4202
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3886
4203
  const parsed = JSON.parse(body);
3887
4204
  if (!parsed || !Array.isArray(parsed.routines)) { res.writeHead(400); res.end('{"error":"routines array required"}'); return; }
3888
- const routinesFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-routines.json`);
4205
+ const _postRoutinesQs = new URL(req.url, 'http://localhost').searchParams;
4206
+ const routinesFile = path.join(path.resolve(_postRoutinesQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs', `${orgName}-routines.json`);
3889
4207
  const tmp = `${routinesFile}.tmp`;
3890
4208
  const payload = { org: orgName, updated_at: new Date().toISOString(), routines: parsed.routines };
3891
4209
  fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
@@ -3896,16 +4214,95 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3896
4214
  return;
3897
4215
  }
3898
4216
 
3899
- // DELETE /api/orgs/:name — delete an org config and all associated data files
3900
- if (req.method === 'DELETE' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}$/i)) {
4217
+ // GET /api/org/:name/filesall files related to an org
4218
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/files$/i)) {
3901
4219
  try {
3902
4220
  const orgName = decodeURIComponent(url.split('/')[3]);
4221
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{"error":"Invalid org name"}'); return; }
4222
+ const _filesQs = new URL(req.url, 'http://localhost').searchParams;
4223
+ const d = path.resolve(_filesQs.get('dir') || projectDir || process.cwd());
4224
+ const orgsDir = path.join(d, '.monomind', 'orgs');
4225
+ const files = [];
4226
+ const seen = new Set();
4227
+ const addFile = (fp, type) => {
4228
+ if (seen.has(fp)) return; seen.add(fp);
4229
+ try { const st = fs.statSync(fp); files.push({ name: path.basename(fp), path: fp, type, size: st.size, mtime: st.mtime.toISOString() }); } catch (_) {}
4230
+ };
4231
+ addFile(path.join(orgsDir, orgName + '.json'), 'config');
4232
+ for (const s of ['-state','-approvals','-goals','-routines','-projects','-members','-issues','-threads','-budgets']) {
4233
+ const fp = path.join(orgsDir, orgName + s + '.json');
4234
+ if (fs.existsSync(fp)) addFile(fp, s.slice(1));
4235
+ }
4236
+ const walkDir = (dir, depth) => {
4237
+ if (depth > 3) return;
4238
+ let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
4239
+ for (const e of entries) {
4240
+ if (e.name.startsWith('.')) continue;
4241
+ const fp = path.join(dir, e.name);
4242
+ if (e.isDirectory()) walkDir(fp, depth + 1);
4243
+ else addFile(fp, 'generated');
4244
+ }
4245
+ };
4246
+ const orgWorkDir = path.join(orgsDir, orgName);
4247
+ if (fs.existsSync(orgWorkDir)) walkDir(orgWorkDir, 0);
4248
+ let orgCfg = null;
4249
+ try { orgCfg = JSON.parse(fs.readFileSync(path.join(orgsDir, orgName + '.json'), 'utf8')); } catch (_) {}
4250
+ if (orgCfg && Array.isArray(orgCfg.roles)) {
4251
+ const agentsDir = path.join(d, '.claude', 'agents');
4252
+ const walkAgents = (dir) => {
4253
+ let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
4254
+ for (const e of entries) {
4255
+ if (e.isDirectory()) { walkAgents(path.join(dir, e.name)); continue; }
4256
+ if (!e.name.endsWith('.md')) continue;
4257
+ const fp = path.join(dir, e.name);
4258
+ const base = e.name.replace('.md', '').toLowerCase();
4259
+ if (orgCfg.roles.some(r => base === (r.id||'').toLowerCase() || base === (r.agent_type||'').toLowerCase() || (r.instructions_file||'').endsWith(e.name))) addFile(fp, 'agent-definition');
4260
+ }
4261
+ };
4262
+ if (fs.existsSync(agentsDir)) walkAgents(agentsDir);
4263
+ }
4264
+ files.sort((a, b) => new Date(b.mtime) - new Date(a.mtime));
4265
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
4266
+ res.end(JSON.stringify(files));
4267
+ } catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); }
4268
+ return;
4269
+ }
4270
+
4271
+ // GET /api/file-content — return raw text content of a .monomind file
4272
+ if (req.method === 'GET' && url === '/api/file-content') {
4273
+ try {
4274
+ const _fcQs = new URL(req.url, 'http://localhost').searchParams;
4275
+ const rawPath = _fcQs.get('path');
4276
+ const baseDir = path.resolve(_fcQs.get('dir') || projectDir || process.cwd());
4277
+ if (!rawPath) { res.writeHead(400); res.end('Missing path'); return; }
4278
+ const resolved = path.resolve(rawPath);
4279
+ // Security: must be inside .monomind of the project dir
4280
+ const monomindDir = path.join(baseDir, '.monomind');
4281
+ if (!resolved.startsWith(monomindDir + path.sep) && resolved !== monomindDir) {
4282
+ res.writeHead(403); res.end('Forbidden'); return;
4283
+ }
4284
+ if (!fs.existsSync(resolved)) { res.writeHead(404); res.end('Not found'); return; }
4285
+ const stat = fs.statSync(resolved);
4286
+ if (!stat.isFile()) { res.writeHead(400); res.end('Not a file'); return; }
4287
+ if (stat.size > 524288) { res.writeHead(413); res.end('File too large'); return; }
4288
+ const content = fs.readFileSync(resolved, 'utf8');
4289
+ res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
4290
+ res.end(content);
4291
+ } catch(_) { res.writeHead(500); res.end('Internal error'); }
4292
+ return;
4293
+ }
4294
+
4295
+ // DELETE /api/orgs/:name — delete an org config and all associated data files
4296
+ if (req.method === 'DELETE' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}(\?.*)?$/i)) {
4297
+ try {
4298
+ const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
3903
4299
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
3904
- const orgsDir = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
4300
+ const _delOrgQs = new URL(req.url, 'http://localhost').searchParams;
4301
+ const orgsDir = path.join(path.resolve(_delOrgQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
3905
4302
  const configFile = path.join(orgsDir, `${orgName}.json`);
3906
4303
  if (!fs.existsSync(configFile)) { res.writeHead(404); res.end('{"error":"org not found"}'); return; }
3907
4304
  // Remove all org-associated files (config + state + data)
3908
- const suffixes = ['', '-state', '-goals', '-routines', '-approvals', '-activity', '-issues', '-members', '-projects', '-workspaces', '-worktrees', '-environments', '-plugins', '-adapters'];
4305
+ const suffixes = ['', '-state', '-goals', '-routines', '-approvals', '-activity', '-issues', '-members', '-projects', '-workspaces', '-worktrees', '-environments', '-plugins', '-adapters', '-budgets', '-threads', '-secrets', '-join-requests', '-bootstrap', '-project-workspaces', '-approval-comments', '-skills'];
3909
4306
  for (const suf of suffixes) {
3910
4307
  const f = path.join(orgsDir, `${orgName}${suf}.json`);
3911
4308
  try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch(_) {}
@@ -3914,6 +4311,18 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3914
4311
  }
3915
4312
  // Remove stop file if present
3916
4313
  try { fs.unlinkSync(path.join(orgsDir, '.stops', `${orgName}.stop`)); } catch(_) {}
4314
+ // Remove org subdirectory under .monomind/orgs/ (legacy flat-file location)
4315
+ try { const orgWorkDir = path.join(orgsDir, orgName); if (fs.existsSync(orgWorkDir)) fs.rmSync(orgWorkDir, { recursive: true, force: true }); } catch(_) {}
4316
+ // Remove org subdirectory under git-safe location (.git/monomind/orgs/<name>/) so run
4317
+ // files written by the worktree-aware path (feat 880f034e) are also cleaned up on delete
4318
+ try {
4319
+ const _delWorkDir = path.resolve(_delOrgQs.get('dir') || projectDir || process.cwd());
4320
+ const _delGitMonoDir = _getGitMonomindDir(_delWorkDir);
4321
+ if (_delGitMonoDir) {
4322
+ const gitOrgDir = path.join(_delGitMonoDir, 'orgs', orgName);
4323
+ if (fs.existsSync(gitOrgDir)) fs.rmSync(gitOrgDir, { recursive: true, force: true });
4324
+ }
4325
+ } catch(_) {}
3917
4326
  // Remove loop prompt file if present (created for scheduled orgs by createorg)
3918
4327
  try { const lpf = path.join(path.resolve(projectDir || process.cwd()), '.monomind', 'loops', `${orgName}.md`); if (fs.existsSync(lpf)) fs.unlinkSync(lpf); } catch(_) {}
3919
4328
  // Emit org:delete event
@@ -3932,13 +4341,15 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3932
4341
  try {
3933
4342
  const orgName = decodeURIComponent(url.split('/')[3]);
3934
4343
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
4344
+ const _stopOrgQs = new URL(req.url, 'http://localhost').searchParams;
4345
+ const _stopOrgBase = path.resolve(_stopOrgQs.get('dir') || projectDir || process.cwd());
3935
4346
  const stopEvent = { type: 'org:stop', org: orgName, ts: Date.now() };
3936
- const dataDir = path.join(projectDir || process.cwd(), 'data');
4347
+ const dataDir = path.join(_stopOrgBase, 'data');
3937
4348
  try { fs.mkdirSync(dataDir, { recursive: true }); } catch(_) {}
3938
4349
  try { fs.appendFileSync(path.join(dataDir, 'mastermind-events.jsonl'), JSON.stringify(stopEvent) + '\n'); } catch(_) {}
3939
4350
  // Write stop marker file for boss agent to detect
3940
4351
  try {
3941
- const stopDir = path.join(projectDir || process.cwd(), '.monomind', 'orgs', '.stops');
4352
+ const stopDir = path.join(_stopOrgBase, '.monomind', 'orgs', '.stops');
3942
4353
  fs.mkdirSync(stopDir, { recursive: true });
3943
4354
  fs.writeFileSync(path.join(stopDir, `${orgName}.stop`), String(Date.now()));
3944
4355
  } catch(_) {}
@@ -3953,7 +4364,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3953
4364
  // POST /api/orgs/:name/copy — copy org config to another project directory
3954
4365
  if (req.method === 'POST' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/copy$/i)) {
3955
4366
  let body = '';
3956
- for await (const chunk of req) body += chunk;
4367
+ for await (const chunk of req) { body += chunk; if (body.length > 2097152) { req.destroy(); break; } }
3957
4368
  try {
3958
4369
  const orgName = decodeURIComponent(url.split('/')[3]);
3959
4370
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid org name' })); return; }
@@ -3962,7 +4373,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3962
4373
  const destination = payload.destination ? String(payload.destination).trim() : '';
3963
4374
  if (!destination) { res.writeHead(400); res.end(JSON.stringify({ error: 'destination is required' })); return; }
3964
4375
  if (!path.isAbsolute(destination)) { res.writeHead(400); res.end(JSON.stringify({ error: 'destination must be an absolute path' })); return; }
3965
- const srcOrgsDir = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
4376
+ const _copyOrgQs = new URL(req.url, 'http://localhost').searchParams;
4377
+ const srcOrgsDir = path.join(path.resolve(_copyOrgQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
3966
4378
  const srcFile = path.join(srcOrgsDir, `${orgName}.json`);
3967
4379
  if (!fs.existsSync(srcFile)) { res.writeHead(404); res.end(JSON.stringify({ error: 'org not found' })); return; }
3968
4380
  const destOrgsDir = path.join(path.resolve(destination), '.monomind', 'orgs');
@@ -3975,16 +4387,87 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3975
4387
  return;
3976
4388
  }
3977
4389
 
4390
+ // GET /api/org/:name/runs — list structured run files for an org
4391
+ if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/runs(\?.*)?$/i.test(url)) {
4392
+ try {
4393
+ const _rQs = new URL(req.url, 'http://localhost').searchParams;
4394
+ const _rOrgName = decodeURIComponent(url.split('/')[3] || '');
4395
+ const _rWorkDir = path.resolve(_rQs.get('dir') || projectDir || process.cwd());
4396
+ const _rMonoDir = _getGitMonomindDir(_rWorkDir) || path.join(_rWorkDir, '.monomind');
4397
+ const _rDir = path.join(_rMonoDir, 'orgs', _rOrgName, 'runs');
4398
+ const runs = [];
4399
+ if (fs.existsSync(_rDir)) {
4400
+ const files = fs.readdirSync(_rDir).filter(f => f.endsWith('.jsonl')).sort().reverse();
4401
+ for (const f of files.slice(0, 50)) {
4402
+ try {
4403
+ const raw = fs.readFileSync(path.join(_rDir, f), 'utf8');
4404
+ const allLines = raw.split('\n').filter(Boolean);
4405
+ const eventCount = allLines.length;
4406
+ const parse = l => { try { return JSON.parse(l); } catch { return null; } };
4407
+ const headEvents = allLines.slice(0, 5).map(parse).filter(Boolean);
4408
+ const tailEvents = allLines.slice(-5).map(parse).filter(Boolean);
4409
+ const first = headEvents.find(e => e.type === 'run:start') || headEvents[0];
4410
+ const last = tailEvents.slice().reverse().find(e => e.type === 'run:complete' || e.type === 'org:complete');
4411
+ const cycles = allLines.filter(l => l.includes('"org:checkpoint"')).length;
4412
+ const lastEvent = tailEvents[tailEvents.length - 1] || headEvents[headEvents.length - 1];
4413
+ const ageMs = lastEvent?.ts ? Date.now() - lastEvent.ts : Infinity;
4414
+ const isStale = !last && ageMs > 30 * 60 * 1000;
4415
+ runs.push({ runId: f.replace('.jsonl', ''), startedAt: first?.ts || 0, endedAt: last?.ts || 0,
4416
+ status: last ? 'complete' : isStale ? 'stale' : 'running',
4417
+ eventCount, cycleCount: cycles, goal: first?.goal || '', bossRole: first?.bossRole || '' });
4418
+ } catch (_) {}
4419
+ }
4420
+ }
4421
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
4422
+ res.end(JSON.stringify(runs));
4423
+ } catch (_) { res.writeHead(500); res.end('[]'); }
4424
+ return;
4425
+ }
4426
+
4427
+ // GET /api/org/:name/runs/:runId — get all events for a specific run
4428
+ if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/runs\/[a-z0-9][a-z0-9_-]{0,79}(\?.*)?$/i.test(url)) {
4429
+ try {
4430
+ const _rvQs = new URL(req.url, 'http://localhost').searchParams;
4431
+ const _rvParts = url.replace(/\?.*$/, '').split('/');
4432
+ const _rvOrgName = decodeURIComponent(_rvParts[3] || '');
4433
+ const _rvRunId = decodeURIComponent(_rvParts[5] || '');
4434
+ const _rvWorkDir = path.resolve(_rvQs.get('dir') || projectDir || process.cwd());
4435
+ const _rvMonoDir = _getGitMonomindDir(_rvWorkDir) || path.join(_rvWorkDir, '.monomind');
4436
+ const _rvFile = path.join(_rvMonoDir, 'orgs', _rvOrgName, 'runs', `${_rvRunId}.jsonl`);
4437
+ if (!fs.existsSync(_rvFile)) { res.writeHead(404); res.end('{"error":"run not found"}'); return; }
4438
+ const events = fs.readFileSync(_rvFile, 'utf8').split('\n').filter(Boolean)
4439
+ .map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
4440
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
4441
+ res.end(JSON.stringify(events));
4442
+ } catch (_) { res.writeHead(500); res.end('[]'); }
4443
+ return;
4444
+ }
4445
+
3978
4446
  // ------------------------------------------------- Mastermind event system
3979
4447
  // POST /api/mastermind/event — ingest event from mastermind skill
3980
4448
  if (req.method === 'POST' && url === '/api/mastermind/event') {
3981
4449
  let body = '';
3982
- for await (const chunk of req) body += chunk;
4450
+ for await (const chunk of req) { body += chunk; if (body.length > 2097152) { req.destroy(); break; } }
3983
4451
  let event = {};
3984
4452
  try { event = JSON.parse(body); } catch (_) {}
3985
4453
  event.ts = event.ts || Date.now();
3986
- // Use project path from event if provided (multi-project support)
3987
- const eventProject = event.project && path.isAbsolute(event.project) ? event.project : null;
4454
+ // Use project path from event if provided (multi-project support).
4455
+ // Security: path.isAbsolute() alone is insufficient — an attacker can
4456
+ // supply event.project="/etc" and cause writes to system directories.
4457
+ // Only accept paths that resolve to an existing directory AND are not
4458
+ // the filesystem root (/), AND are not obviously system paths.
4459
+ // Cap to 4096 chars to prevent OOM from huge path strings.
4460
+ const _rawProject = event.project;
4461
+ let eventProject = null;
4462
+ if (typeof _rawProject === 'string' && _rawProject.length > 0 && _rawProject.length <= 4096
4463
+ && path.isAbsolute(_rawProject)) {
4464
+ // Reject filesystem root and common system directories
4465
+ const _norm = path.resolve(_rawProject);
4466
+ const _systemPaths = ['/', '/etc', '/usr', '/bin', '/sbin', '/lib', '/lib64', '/boot', '/dev', '/sys', '/proc', '/tmp'];
4467
+ if (!_systemPaths.includes(_norm) && !_systemPaths.some(p => _norm.startsWith(p + '/'))) {
4468
+ eventProject = _norm;
4469
+ }
4470
+ }
3988
4471
  const root = eventProject || projectDir || process.cwd();
3989
4472
  const dataDir = path.join(root, 'data');
3990
4473
  try { fs.mkdirSync(dataDir, { recursive: true }); } catch (_) {}
@@ -3998,6 +4481,28 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3998
4481
  } catch (_) {}
3999
4482
  }
4000
4483
  try { fs.appendFileSync(path.join(dataDir, 'mastermind-events.jsonl'), JSON.stringify(event) + '\n'); } catch (_) {}
4484
+ // Track active runs and route org events to run files
4485
+ if (event.org) {
4486
+ const _orgKey = String(event.org).trim();
4487
+ // Any event with both org+runId updates the active run map (run:start written directly to file so org:start is first via curl)
4488
+ if (event.runId) activeOrgRuns.set(_orgKey, String(event.runId).trim());
4489
+ else if (activeOrgRuns.has(_orgKey)) event.runId = activeOrgRuns.get(_orgKey);
4490
+ if (event.type === 'run:complete' || event.type === 'org:complete') activeOrgRuns.delete(_orgKey);
4491
+ }
4492
+ // Persist to git-safe run file (survives branch switches + shared across worktrees)
4493
+ if (event.org && event.runId) {
4494
+ try {
4495
+ const _orn = String(event.org).trim();
4496
+ const _rid = String(event.runId).trim();
4497
+ if (_orn.length > 0 && _orn.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(_orn)
4498
+ && _rid.length > 0 && _rid.length <= 80 && /^[a-z0-9][a-z0-9_-]*$/i.test(_rid)) {
4499
+ const _monoDir = _getGitMonomindDir(root) || path.join(root, '.monomind');
4500
+ const _runDir = path.join(_monoDir, 'orgs', _orn, 'runs');
4501
+ fs.mkdirSync(_runDir, { recursive: true });
4502
+ fs.appendFileSync(path.join(_runDir, `${_rid}.jsonl`), JSON.stringify(event) + '\n');
4503
+ }
4504
+ } catch (_) {}
4505
+ }
4001
4506
  // Persist session
4002
4507
  try {
4003
4508
  const sessFile = path.join(dataDir, 'mastermind-sessions.json');
@@ -4016,12 +4521,19 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4016
4521
  }
4017
4522
  }
4018
4523
  fs.writeFileSync(sessFile, JSON.stringify(sessions.slice(0, 50), null, 2));
4019
- // Also write individual session file for direct traceability
4524
+ // Also write individual session file for direct traceability.
4525
+ // Security: validate event.session before using it as a filename to
4526
+ // prevent path traversal (e.g. "../../../etc/cron.d/payload").
4020
4527
  const sessionObj = sessions.find(s => s.id === event.session);
4021
4528
  if (sessionObj) {
4022
4529
  const sessDir = path.join(dataDir, 'sessions');
4023
4530
  try { fs.mkdirSync(sessDir, { recursive: true }); } catch (_) {}
4024
- try { fs.writeFileSync(path.join(sessDir, `${event.session}.json`), JSON.stringify(sessionObj, null, 2)); } catch (_) {}
4531
+ try {
4532
+ const _sid = String(event.session || '').trim();
4533
+ if (_sid.length > 0 && _sid.length <= 128 && /^[a-zA-Z0-9_.-]+$/.test(_sid)) {
4534
+ fs.writeFileSync(path.join(sessDir, `${_sid}.json`), JSON.stringify(sessionObj, null, 2));
4535
+ }
4536
+ } catch (_) {}
4025
4537
  }
4026
4538
  } catch (_) {}
4027
4539
  // For org:stop events, write a stop marker the boss agent can detect
@@ -4198,6 +4710,75 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4198
4710
  return;
4199
4711
  }
4200
4712
 
4713
+ // GET /api/status — live system snapshot for dashboard polling
4714
+ if (req.method === 'GET' && url === '/api/status') {
4715
+ try {
4716
+ const root = projectDir || process.cwd();
4717
+ // Active org runs: { orgName -> runId }
4718
+ const orgRuns = {};
4719
+ activeOrgRuns.forEach((runId, org) => { orgRuns[org] = runId; });
4720
+ // Recent events (last 10)
4721
+ let recentEvents = [];
4722
+ try {
4723
+ const evPath = path.join(root, 'data', 'mastermind-events.jsonl');
4724
+ const lines = fs.readFileSync(evPath, 'utf8').split('\n').filter(l => l.trim()).slice(-10);
4725
+ recentEvents = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
4726
+ } catch(_) {}
4727
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4728
+ res.end(JSON.stringify({
4729
+ ts: Date.now(),
4730
+ uptime: process.uptime(),
4731
+ sseClients: mmSseClients.size,
4732
+ activeOrgs: Object.keys(orgRuns).length,
4733
+ orgRuns,
4734
+ recentEvents,
4735
+ }));
4736
+ } catch(err) {
4737
+ res.writeHead(500, { 'Content-Type': 'application/json' });
4738
+ res.end(JSON.stringify({ error: err.message }));
4739
+ }
4740
+ return;
4741
+ }
4742
+
4743
+ // GET /api/orgs/:name/runs/current — events from the active run file for an org
4744
+ if (req.method === 'GET' && /^\/api\/orgs\/[^/]+\/runs\/current$/.test(url)) {
4745
+ try {
4746
+ const orgName = decodeURIComponent(url.split('/')[3]);
4747
+ const root = projectDir || process.cwd();
4748
+ // Validate orgName
4749
+ if (!orgName || orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
4750
+ res.writeHead(400); res.end('{"error":"invalid org name"}'); return;
4751
+ }
4752
+ const runId = activeOrgRuns.get(orgName);
4753
+ const monoDir = _getGitMonomindDir(root) || path.join(root, '.monomind');
4754
+ // Try active run first, then fall back to most recent run file
4755
+ let runFile = null;
4756
+ if (runId) {
4757
+ const candidate = path.join(monoDir, 'orgs', orgName, 'runs', `${runId}.jsonl`);
4758
+ if (fs.existsSync(candidate)) runFile = candidate;
4759
+ }
4760
+ if (!runFile) {
4761
+ const runsDir = path.join(monoDir, 'orgs', orgName, 'runs');
4762
+ if (fs.existsSync(runsDir)) {
4763
+ const files = fs.readdirSync(runsDir).filter(f => f.endsWith('.jsonl'));
4764
+ if (files.length) {
4765
+ files.sort();
4766
+ runFile = path.join(runsDir, files[files.length - 1]);
4767
+ }
4768
+ }
4769
+ }
4770
+ if (!runFile) { res.writeHead(404); res.end('{"events":[],"runId":null}'); return; }
4771
+ const detectedRunId = path.basename(runFile, '.jsonl');
4772
+ const lines = fs.readFileSync(runFile, 'utf8').split('\n').filter(l => l.trim()).slice(-100);
4773
+ const events = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
4774
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4775
+ res.end(JSON.stringify({ runId: detectedRunId, events, active: activeOrgRuns.has(orgName) }));
4776
+ } catch(err) {
4777
+ res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
4778
+ }
4779
+ return;
4780
+ }
4781
+
4201
4782
  // GET /api/mastermind/metrics — aggregate system metrics from token-summary and swarm-activity
4202
4783
  if (req.method === 'GET' && url === '/api/mastermind/metrics') {
4203
4784
  try {