mstro-app 0.5.1 → 0.5.6

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 (283) hide show
  1. package/PRIVACY.md +9 -9
  2. package/README.md +71 -28
  3. package/bin/commands/config.js +1 -1
  4. package/bin/mstro.js +55 -4
  5. package/dist/server/cli/eta-estimator.d.ts +55 -0
  6. package/dist/server/cli/eta-estimator.d.ts.map +1 -0
  7. package/dist/server/cli/eta-estimator.js +222 -0
  8. package/dist/server/cli/eta-estimator.js.map +1 -0
  9. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
  10. package/dist/server/cli/headless/claude-invoker-process.js +9 -1
  11. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
  12. package/dist/server/cli/headless/mcp-config.d.ts +22 -5
  13. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
  14. package/dist/server/cli/headless/mcp-config.js +7 -5
  15. package/dist/server/cli/headless/mcp-config.js.map +1 -1
  16. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  17. package/dist/server/cli/headless/runner.js +19 -0
  18. package/dist/server/cli/headless/runner.js.map +1 -1
  19. package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
  20. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  21. package/dist/server/cli/headless/stall-assessor.js +64 -9
  22. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  23. package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
  24. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  25. package/dist/server/cli/headless/tool-watchdog.js +19 -12
  26. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  27. package/dist/server/cli/headless/types.d.ts +16 -1
  28. package/dist/server/cli/headless/types.d.ts.map +1 -1
  29. package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
  30. package/dist/server/cli/improvisation-history-store.js +5 -1
  31. package/dist/server/cli/improvisation-history-store.js.map +1 -1
  32. package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
  33. package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
  34. package/dist/server/cli/improvisation-output-queue.js +30 -7
  35. package/dist/server/cli/improvisation-output-queue.js.map +1 -1
  36. package/dist/server/cli/improvisation-session-manager.d.ts +35 -0
  37. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  38. package/dist/server/cli/improvisation-session-manager.js +58 -1
  39. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  40. package/dist/server/cli/improvisation-types.d.ts +9 -0
  41. package/dist/server/cli/improvisation-types.d.ts.map +1 -1
  42. package/dist/server/cli/improvisation-types.js.map +1 -1
  43. package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -1
  44. package/dist/server/cli/retry/retry-runner-factory.js +1 -0
  45. package/dist/server/cli/retry/retry-runner-factory.js.map +1 -1
  46. package/dist/server/engines/EngineEvent.d.ts +126 -0
  47. package/dist/server/engines/EngineEvent.d.ts.map +1 -0
  48. package/dist/server/engines/EngineEvent.js +11 -0
  49. package/dist/server/engines/EngineEvent.js.map +1 -0
  50. package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
  51. package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
  52. package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
  53. package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
  54. package/dist/server/engines/factory.d.ts +21 -0
  55. package/dist/server/engines/factory.d.ts.map +1 -0
  56. package/dist/server/engines/factory.js +152 -0
  57. package/dist/server/engines/factory.js.map +1 -0
  58. package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
  59. package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
  60. package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
  61. package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
  62. package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
  63. package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
  64. package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
  65. package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
  66. package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
  67. package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
  68. package/dist/server/engines/opencode/model-catalog.js +141 -0
  69. package/dist/server/engines/opencode/model-catalog.js.map +1 -0
  70. package/dist/server/engines/types.d.ts +146 -0
  71. package/dist/server/engines/types.d.ts.map +1 -0
  72. package/dist/server/engines/types.js +4 -0
  73. package/dist/server/engines/types.js.map +1 -0
  74. package/dist/server/index.js +9 -2
  75. package/dist/server/index.js.map +1 -1
  76. package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
  77. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
  78. package/dist/server/mcp/bouncer-haiku.js +8 -124
  79. package/dist/server/mcp/bouncer-haiku.js.map +1 -1
  80. package/dist/server/mcp/bouncer-integration.d.ts +45 -0
  81. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  82. package/dist/server/mcp/bouncer-integration.js +69 -5
  83. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  84. package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
  85. package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
  86. package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
  87. package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
  88. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
  89. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
  90. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
  91. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
  92. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
  93. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
  94. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
  95. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
  96. package/dist/server/mcp/classifier/factory.d.ts +70 -0
  97. package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
  98. package/dist/server/mcp/classifier/factory.js +155 -0
  99. package/dist/server/mcp/classifier/factory.js.map +1 -0
  100. package/dist/server/mcp/server.js +52 -0
  101. package/dist/server/mcp/server.js.map +1 -1
  102. package/dist/server/routes/index.d.ts +1 -0
  103. package/dist/server/routes/index.d.ts.map +1 -1
  104. package/dist/server/routes/index.js +1 -0
  105. package/dist/server/routes/index.js.map +1 -1
  106. package/dist/server/routes/internal.d.ts +16 -0
  107. package/dist/server/routes/internal.d.ts.map +1 -0
  108. package/dist/server/routes/internal.js +94 -0
  109. package/dist/server/routes/internal.js.map +1 -0
  110. package/dist/server/services/plan/agent-resolver.d.ts +26 -0
  111. package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
  112. package/dist/server/services/plan/agent-resolver.js +102 -0
  113. package/dist/server/services/plan/agent-resolver.js.map +1 -0
  114. package/dist/server/services/plan/composer.d.ts.map +1 -1
  115. package/dist/server/services/plan/composer.js +59 -11
  116. package/dist/server/services/plan/composer.js.map +1 -1
  117. package/dist/server/services/plan/executor.d.ts.map +1 -1
  118. package/dist/server/services/plan/executor.js +3 -1
  119. package/dist/server/services/plan/executor.js.map +1 -1
  120. package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
  121. package/dist/server/services/plan/issue-prompt-builder.js +33 -1
  122. package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
  123. package/dist/server/services/plan/parser-core.d.ts.map +1 -1
  124. package/dist/server/services/plan/parser-core.js +1 -0
  125. package/dist/server/services/plan/parser-core.js.map +1 -1
  126. package/dist/server/services/plan/types.d.ts +1 -0
  127. package/dist/server/services/plan/types.d.ts.map +1 -1
  128. package/dist/server/services/runtime-info.d.ts +3 -0
  129. package/dist/server/services/runtime-info.d.ts.map +1 -0
  130. package/dist/server/services/runtime-info.js +21 -0
  131. package/dist/server/services/runtime-info.js.map +1 -0
  132. package/dist/server/services/settings.d.ts +76 -2
  133. package/dist/server/services/settings.d.ts.map +1 -1
  134. package/dist/server/services/settings.js +127 -4
  135. package/dist/server/services/settings.js.map +1 -1
  136. package/dist/server/services/websocket/ask-user-question-bridge.d.ts +32 -0
  137. package/dist/server/services/websocket/ask-user-question-bridge.d.ts.map +1 -0
  138. package/dist/server/services/websocket/ask-user-question-bridge.js +115 -0
  139. package/dist/server/services/websocket/ask-user-question-bridge.js.map +1 -0
  140. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
  141. package/dist/server/services/websocket/git-branch-handlers.js +19 -6
  142. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
  143. package/dist/server/services/websocket/handler.d.ts +25 -1
  144. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  145. package/dist/server/services/websocket/handler.js +84 -2
  146. package/dist/server/services/websocket/handler.js.map +1 -1
  147. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
  148. package/dist/server/services/websocket/quality-complexity.js +78 -26
  149. package/dist/server/services/websocket/quality-complexity.js.map +1 -1
  150. package/dist/server/services/websocket/quality-eta.d.ts +47 -0
  151. package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
  152. package/dist/server/services/websocket/quality-eta.js +110 -0
  153. package/dist/server/services/websocket/quality-eta.js.map +1 -0
  154. package/dist/server/services/websocket/quality-grading.d.ts +27 -4
  155. package/dist/server/services/websocket/quality-grading.d.ts.map +1 -1
  156. package/dist/server/services/websocket/quality-grading.js +369 -201
  157. package/dist/server/services/websocket/quality-grading.js.map +1 -1
  158. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  159. package/dist/server/services/websocket/quality-handlers.js +145 -7
  160. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  161. package/dist/server/services/websocket/quality-operations.d.ts +34 -0
  162. package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
  163. package/dist/server/services/websocket/quality-operations.js +47 -0
  164. package/dist/server/services/websocket/quality-operations.js.map +1 -0
  165. package/dist/server/services/websocket/quality-persistence.d.ts +9 -0
  166. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
  167. package/dist/server/services/websocket/quality-persistence.js +10 -0
  168. package/dist/server/services/websocket/quality-persistence.js.map +1 -1
  169. package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
  170. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
  171. package/dist/server/services/websocket/quality-review-agent.js +105 -56
  172. package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
  173. package/dist/server/services/websocket/quality-service.d.ts +9 -1
  174. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  175. package/dist/server/services/websocket/quality-service.js +334 -14
  176. package/dist/server/services/websocket/quality-service.js.map +1 -1
  177. package/dist/server/services/websocket/quality-tools.d.ts +21 -0
  178. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
  179. package/dist/server/services/websocket/quality-tools.js +49 -0
  180. package/dist/server/services/websocket/quality-tools.js.map +1 -1
  181. package/dist/server/services/websocket/quality-types.d.ts +35 -2
  182. package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
  183. package/dist/server/services/websocket/quality-types.js +1 -1
  184. package/dist/server/services/websocket/quality-types.js.map +1 -1
  185. package/dist/server/services/websocket/session-handlers.d.ts +3 -1
  186. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  187. package/dist/server/services/websocket/session-handlers.js +60 -9
  188. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  189. package/dist/server/services/websocket/session-history.js +3 -0
  190. package/dist/server/services/websocket/session-history.js.map +1 -1
  191. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
  192. package/dist/server/services/websocket/session-initialization.js +158 -42
  193. package/dist/server/services/websocket/session-initialization.js.map +1 -1
  194. package/dist/server/services/websocket/session-registry.d.ts +25 -0
  195. package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
  196. package/dist/server/services/websocket/session-registry.js +19 -0
  197. package/dist/server/services/websocket/session-registry.js.map +1 -1
  198. package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
  199. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
  200. package/dist/server/services/websocket/settings-handlers.js +35 -4
  201. package/dist/server/services/websocket/settings-handlers.js.map +1 -1
  202. package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
  203. package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
  204. package/dist/server/services/websocket/tab-broadcast.js +10 -2
  205. package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
  206. package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
  207. package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
  208. package/dist/server/services/websocket/tab-event-buffer.js +138 -12
  209. package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
  210. package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
  211. package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
  212. package/dist/server/services/websocket/tab-event-replay.js +55 -2
  213. package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
  214. package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
  215. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
  216. package/dist/server/services/websocket/tab-handlers.js +47 -2
  217. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  218. package/dist/server/services/websocket/types.d.ts +67 -7
  219. package/dist/server/services/websocket/types.d.ts.map +1 -1
  220. package/dist/server/services/websocket/types.js +12 -6
  221. package/dist/server/services/websocket/types.js.map +1 -1
  222. package/package.json +5 -3
  223. package/server/cli/eta-estimator.ts +249 -0
  224. package/server/cli/headless/claude-invoker-process.ts +9 -1
  225. package/server/cli/headless/mcp-config.ts +30 -5
  226. package/server/cli/headless/runner.ts +21 -0
  227. package/server/cli/headless/stall-assessor.ts +93 -0
  228. package/server/cli/headless/tool-watchdog.ts +21 -0
  229. package/server/cli/headless/types.ts +16 -1
  230. package/server/cli/improvisation-history-store.ts +4 -1
  231. package/server/cli/improvisation-output-queue.ts +29 -7
  232. package/server/cli/improvisation-session-manager.ts +63 -1
  233. package/server/cli/improvisation-types.ts +9 -0
  234. package/server/cli/retry/retry-runner-factory.ts +1 -0
  235. package/server/engines/EngineEvent.ts +156 -0
  236. package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
  237. package/server/engines/factory.ts +176 -0
  238. package/server/engines/opencode/OpenCodeEngine.ts +786 -0
  239. package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
  240. package/server/engines/opencode/model-catalog.ts +217 -0
  241. package/server/engines/types.ts +173 -0
  242. package/server/index.ts +9 -1
  243. package/server/mcp/bouncer-haiku.ts +21 -145
  244. package/server/mcp/bouncer-integration.ts +107 -5
  245. package/server/mcp/classifier/BouncerClassifier.ts +40 -0
  246. package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
  247. package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
  248. package/server/mcp/classifier/factory.ts +195 -0
  249. package/server/mcp/server.ts +57 -0
  250. package/server/routes/index.ts +1 -0
  251. package/server/routes/internal.ts +112 -0
  252. package/server/services/plan/agent-resolver.ts +115 -0
  253. package/server/services/plan/agents/code-review.md +38 -8
  254. package/server/services/plan/composer.ts +63 -11
  255. package/server/services/plan/executor.ts +3 -1
  256. package/server/services/plan/issue-prompt-builder.ts +39 -1
  257. package/server/services/plan/parser-core.ts +1 -0
  258. package/server/services/plan/types.ts +4 -0
  259. package/server/services/runtime-info.ts +24 -0
  260. package/server/services/settings.ts +161 -4
  261. package/server/services/websocket/ask-user-question-bridge.ts +148 -0
  262. package/server/services/websocket/git-branch-handlers.ts +20 -6
  263. package/server/services/websocket/handler.ts +89 -2
  264. package/server/services/websocket/quality-complexity.ts +80 -26
  265. package/server/services/websocket/quality-eta.ts +155 -0
  266. package/server/services/websocket/quality-grading.ts +445 -222
  267. package/server/services/websocket/quality-handlers.ts +153 -7
  268. package/server/services/websocket/quality-operations.ts +72 -0
  269. package/server/services/websocket/quality-persistence.ts +17 -0
  270. package/server/services/websocket/quality-review-agent.ts +154 -64
  271. package/server/services/websocket/quality-service.ts +361 -13
  272. package/server/services/websocket/quality-tools.ts +51 -0
  273. package/server/services/websocket/quality-types.ts +41 -2
  274. package/server/services/websocket/session-handlers.ts +67 -10
  275. package/server/services/websocket/session-history.ts +3 -0
  276. package/server/services/websocket/session-initialization.ts +189 -46
  277. package/server/services/websocket/session-registry.ts +37 -0
  278. package/server/services/websocket/settings-handlers.ts +41 -4
  279. package/server/services/websocket/tab-broadcast.ts +10 -2
  280. package/server/services/websocket/tab-event-buffer.ts +143 -11
  281. package/server/services/websocket/tab-event-replay.ts +70 -3
  282. package/server/services/websocket/tab-handlers.ts +53 -5
  283. package/server/services/websocket/types.ts +85 -7
package/PRIVACY.md CHANGED
@@ -220,7 +220,7 @@ We ensure that such transfers are subject to appropriate safeguards as required
220
220
  - **Standard Contractual Clauses (SCCs):** We rely on the European Commission-approved Standard Contractual Clauses (including, where applicable, the UK International Data Transfer Addendum) as the transfer mechanism for personal data transferred from the EU/EEA/UK to the United States.
221
221
  - **Processor agreements:** All third-party processors who receive personal data from EU/EEA/UK users are required to maintain SCCs or another valid transfer mechanism.
222
222
 
223
- You may request a copy of the applicable transfer safeguards by contacting us at privacy@mstro.app.
223
+ You may request a copy of the applicable transfer safeguards by contacting us at bravo@mstro.app.
224
224
 
225
225
  ---
226
226
 
@@ -260,9 +260,9 @@ If you are located in the European Union, United Kingdom, or EEA, you have the f
260
260
 
261
261
  **Response timeframe:** We will respond to all valid data subject requests within **30 days** of receipt. Where requests are complex or numerous, we may extend this period by a further two months, in which case we will notify you within the initial 30-day period.
262
262
 
263
- **How to exercise your rights:** Submit a request by email to privacy@mstro.app. We may need to verify your identity before processing your request.
263
+ **How to exercise your rights:** Submit a request by email to bravo@mstro.app. We may need to verify your identity before processing your request.
264
264
 
265
- **Residents of other jurisdictions:** If you are a California resident, you may have additional rights under the CCPA/CPRA, including the right to know, delete, correct, and opt out of sale (we do not sell data). Contact us at privacy@mstro.app for jurisdiction-specific requests.
265
+ **Residents of other jurisdictions:** If you are a California resident, you may have additional rights under the CCPA/CPRA, including the right to know, delete, correct, and opt out of sale (we do not sell data). Contact us at bravo@mstro.app for jurisdiction-specific requests.
266
266
 
267
267
  ---
268
268
 
@@ -273,7 +273,7 @@ If you believe we have not handled your personal data in accordance with applica
273
273
  - **EU:** Contact the data protection authority in your EU member state. A list of EU supervisory authorities is available at: https://edpb.europa.eu/about-edpb/about-edpb/members_en
274
274
  - **UK:** Contact the Information Commissioner's Office (ICO) at https://ico.org.uk
275
275
 
276
- We ask that you contact us first at privacy@mstro.app so that we have the opportunity to address your concern before you escalate to a supervisory authority.
276
+ We ask that you contact us first at bravo@mstro.app so that we have the opportunity to address your concern before you escalate to a supervisory authority.
277
277
 
278
278
  ---
279
279
 
@@ -281,7 +281,7 @@ We ask that you contact us first at privacy@mstro.app so that we have the opport
281
281
 
282
282
  At Mstro's current scale and given the nature of our processing activities, we are not required to appoint a Data Protection Officer (DPO) under Article 37 of the GDPR. We nonetheless take privacy obligations seriously and have designated a privacy contact to handle data protection matters.
283
283
 
284
- **Privacy contact:** privacy@mstro.app
284
+ **Privacy contact:** bravo@mstro.app
285
285
 
286
286
  If our processing activities change in a way that triggers the DPO requirement, we will appoint a DPO and update this policy accordingly.
287
287
 
@@ -317,7 +317,7 @@ Breach notifications to affected users will be sent to the email address associa
317
317
 
318
318
  ## 14. Children's Privacy
319
319
 
320
- **General use:** The Service is not directed to children under the age of **13**. We do not knowingly collect personal data from children under 13. If you believe a child under 13 has provided us with personal data, please contact us at privacy@mstro.app and we will promptly delete that information.
320
+ **General use:** The Service is not directed to children under the age of **13**. We do not knowingly collect personal data from children under 13. If you believe a child under 13 has provided us with personal data, please contact us at bravo@mstro.app and we will promptly delete that information.
321
321
 
322
322
  **Payment features:** Access to paid features and any functionality involving financial transactions requires users to be at least **18 years of age** (or the age of majority in their jurisdiction, if higher). We do not knowingly permit minors to engage in payment-related activities on the platform.
323
323
 
@@ -327,7 +327,7 @@ If you are a parent or guardian and believe your child has used or registered fo
327
327
 
328
328
  ## 15. Source Available
329
329
 
330
- The Mstro CLI is source-available software, licensed under the **PolyForm Noncommercial License 1.0.0**. The source code is available at https://github.com/mstro-app/mstro.
330
+ The Mstro CLI is source-available software, licensed under the **PolyForm Noncommercial License 1.0.0**. The source code is available at https://github.com/mstroapp/mstro.
331
331
 
332
332
  Your use of the CLI source code is governed by the PolyForm Noncommercial 1.0.0 terms (see the LICENSE file in the repository). This Privacy Policy applies to your use of the hosted Service (mstro.app platform, relay server) and any personal data processed in connection with your Mstro account, regardless of which client you use.
333
333
 
@@ -351,9 +351,9 @@ Your continued use of the Service after the effective date of a revised policy c
351
351
 
352
352
  For privacy-related questions, data subject requests, or concerns about this policy:
353
353
 
354
- **Email:** privacy@mstro.app
354
+ **Email:** bravo@mstro.app
355
355
  **Website:** https://mstro.app
356
- **GitHub Issues:** https://github.com/mstro-app/mstro/issues
356
+ **GitHub Issues:** https://github.com/mstroapp/mstro/issues
357
357
 
358
358
  **Mstro, Inc.**
359
359
  United States
package/README.md CHANGED
@@ -1,37 +1,43 @@
1
- # mstro-app
1
+ <div align="center">
2
2
 
3
- Browser-based IDE + AI agent orchestration CLI. Run Claude Code in parallel across git worktrees, auto-approve safe tools with the Security Bouncer, and control long-running AI work from any device at [mstro.app](https://mstro.app).
3
+ <img src="https://mstro.app/mstro-icon.svg" alt="Mstro" width="96" height="96" />
4
4
 
5
- > **Free for the first 1,000 users.** No credit card. Bring your own Anthropic API key.
5
+ # Mstro
6
6
 
7
- > **Start in 30 seconds:**
8
- >
9
- > ```bash
10
- > curl -fsSL install.mstro.app | sh
11
- > ```
12
- >
13
- > Then run `mstro` in any project directory. Open [mstro.app](https://mstro.app). Start building.
7
+ *Browser-based IDE and AI agent orchestration for Claude Code.*
14
8
 
15
- ## What Mstro Does
16
-
17
- ### 1. Browser-based IDE for remote machines
9
+ [![npm version](https://img.shields.io/npm/v/mstro-app?style=flat-square&color=000)](https://www.npmjs.com/package/mstro-app)
10
+ [![npm downloads](https://img.shields.io/npm/dm/mstro-app?style=flat-square&color=000)](https://www.npmjs.com/package/mstro-app)
11
+ [![Node version](https://img.shields.io/node/v/mstro-app?style=flat-square)](https://nodejs.org/)
12
+ [![License](https://img.shields.io/badge/license-PolyForm--NC-blue?style=flat-square)](./LICENSE)
13
+ [![Built for Claude Code](https://img.shields.io/badge/built%20for-Claude%20Code-orange?style=flat-square)](https://docs.anthropic.com/en/docs/claude-code)
18
14
 
19
- Open [mstro.app](https://mstro.app) and connect to Claude Code running on your laptop, cloud VMs, or servers. Chat, edit files, use git, run terminals. All from any browser, on any device. Your code stays on your hardware.
15
+ [Website](https://mstro.app) · [Blog](https://mstro.app/blog) · [Compare](https://mstro.app/compare) · [Security](./SECURITY.md)
20
16
 
21
- ### 2. Long-running AI tasks without babysitting
17
+ </div>
22
18
 
23
- Start a complex task and walk away. The Security Bouncer handles every permission decision automatically. A three-layer watchdog detects stalls, kills frozen processes, and recovers. Come back to finished work.
19
+ <div align="center">
20
+ <img src="https://mstro.app/screenshots/pm-desktop.png" alt="Mstro PM board running in the browser" width="900" />
21
+ </div>
24
22
 
25
- ### 3. One prompt to a full kanban board of parallel AI agents
23
+ ---
26
24
 
27
- Describe what you want. The PM board breaks it into a kanban board of tasks, then AI agent teams execute them in parallel on separate git worktrees. Track progress in real time. What takes a solo developer a week ships in hours.
25
+ Run Claude Code in parallel across git worktrees, auto-approve safe tools with the Security Bouncer, and control long-running AI work from any device at [mstro.app](https://mstro.app). Your code never leaves your computer.
28
26
 
29
- ### 4. One prompt to an autonomous business
27
+ > **Free for the first 1,000 users.** No credit card. Bring your own Anthropic API key.
30
28
 
31
- The long-term direction: self-managing AI-powered businesses that optimize for profit. Today Mstro ships the agent orchestration layer that makes it possible.
29
+ ---
32
30
 
33
31
  ## Quick Start
34
32
 
33
+ > **Start in 30 seconds:**
34
+ >
35
+ > ```bash
36
+ > curl -fsSL install.mstro.app | sh
37
+ > ```
38
+ >
39
+ > Then run `mstro` in any project directory. Open [mstro.app](https://mstro.app). Start building.
40
+
35
41
  **Prerequisites:**
36
42
 
37
43
  - Node.js 18+ (check with `node --version`, [download here](https://nodejs.org/))
@@ -61,13 +67,19 @@ Run `mstro` on multiple machines. Each one appears as a separate workspace.
61
67
 
62
68
  Stop with `Ctrl+C`.
63
69
 
64
- ## How It Works
70
+ ---
65
71
 
66
- ```
67
- Browser (mstro.app) <--WS--> Platform Server (relay) <--WS--> mstro CLI (your machine) --> Claude Code
68
- ```
72
+ ## What Mstro Does
69
73
 
70
- Your code never leaves your computer. The browser is a window into what's happening on your machines.
74
+ **1. Browser-based IDE for remote machines.** Open [mstro.app](https://mstro.app) and connect to Claude Code running on your laptop, cloud VMs, or servers. Chat, edit files, use git, run terminals. All from any browser, on any device. Your code stays on your hardware.
75
+
76
+ **2. Long-running AI tasks without babysitting.** Start a complex task and walk away. The Security Bouncer handles every permission decision automatically. A three-layer watchdog detects stalls, kills frozen processes, and recovers. Come back to finished work.
77
+
78
+ **3. One prompt to a full kanban board of parallel AI agents.** Describe what you want. The PM board breaks it into a kanban board of tasks, then AI agent teams execute them in parallel on separate git worktrees. Track progress in real time. What takes a solo developer a week ships in hours.
79
+
80
+ **4. One prompt to an autonomous business.** The long-term direction: self-managing AI-powered businesses that optimize for profit. Today Mstro ships the agent orchestration layer that makes it possible.
81
+
82
+ ---
71
83
 
72
84
  ## Features
73
85
 
@@ -81,6 +93,16 @@ Your code never leaves your computer. The browser is a window into what's happen
81
93
  | **Terminal** | Full PTY shell access to any machine from your browser |
82
94
  | **Shared Apps** | Invite others with view-only, project control, or full machine access |
83
95
 
96
+ ## How It Works
97
+
98
+ ```
99
+ Browser (mstro.app) <--WS--> Platform Server (relay) <--WS--> mstro CLI (your machine) --> Claude Code
100
+ ```
101
+
102
+ Your code never leaves your computer. The browser is a window into what's happening on your machines.
103
+
104
+ ---
105
+
84
106
  ## Security
85
107
 
86
108
  The Security Bouncer makes permission decisions so you don't have to sit there clicking "Allow."
@@ -102,6 +124,8 @@ Three safety layers run continuously during AI sessions:
102
124
  - **Stall Assessor**: heuristic + AI analysis detects stuck processes
103
125
  - **Tool Watchdog**: per-tool adaptive timeouts using RFC 6298 EMA, with custom profiles for long-running operations (WebFetch 3m, Bash 5m, Task 15m)
104
126
 
127
+ ---
128
+
105
129
  ## PM Board
106
130
 
107
131
  The PM board turns a single prompt into a managed project:
@@ -114,7 +138,8 @@ The PM board turns a single prompt into a managed project:
114
138
 
115
139
  Configurable parallel execution (max concurrent agents), custom review criteria per board, and board-scoped artifacts (progress logs, output files, review results).
116
140
 
117
- ### Custom Review Agents
141
+ <details>
142
+ <summary><strong>Custom Review Agents</strong> — override review prompts per board</summary>
118
143
 
119
144
  When a task moves to "In Review", an AI review agent checks the work. There are three built-in agents:
120
145
 
@@ -162,7 +187,8 @@ Output EXACTLY one JSON object on its own line (no markdown fencing):
162
187
  {"passed": true, "checks": [{"name": "criteria_met", "passed": true, "details": "..."}]}
163
188
  ```
164
189
 
165
- Variables you can use:
190
+ <details>
191
+ <summary>Available template variables</summary>
166
192
 
167
193
  | Variable | Available in | Description |
168
194
  |----------|-------------|-------------|
@@ -176,8 +202,12 @@ Variables you can use:
176
202
  | `review_criteria` | review-custom | Board-level review criteria text |
177
203
  | `read_instruction` | review-custom | Read instruction (changes based on task type) |
178
204
 
205
+ </details>
206
+
179
207
  Resolution order: board agent file > system default > hardcoded fallback. If there's no board override, the built-in agents run.
180
208
 
209
+ </details>
210
+
181
211
  ## Quality
182
212
 
183
213
  Quality analysis runs across your codebase:
@@ -188,6 +218,8 @@ Quality analysis runs across your codebase:
188
218
  - **Severity scoring**: findings tagged with severity, category, file paths, and line numbers
189
219
  - **Automated fixes**: AI can fix identified issues with progress tracking
190
220
 
221
+ ---
222
+
191
223
  ## CLI Reference
192
224
 
193
225
  ```bash
@@ -230,8 +262,13 @@ Stored in `~/.mstro/`:
230
262
  | `settings.json` | Model selection, preferences |
231
263
  | `session-registry.json` | Tab-to-session mapping |
232
264
 
265
+ ---
266
+
233
267
  ## Architecture
234
268
 
269
+ <details>
270
+ <summary><strong>Source layout</strong> — full file tree of subsystems</summary>
271
+
235
272
  ```
236
273
  server/
237
274
  index.ts # Hono app entry, port detection, WebSocket setup
@@ -259,6 +296,8 @@ bin/
259
296
  commands/ # login, logout, status, whoami, config
260
297
  ```
261
298
 
299
+ </details>
300
+
262
301
  ### Key Subsystems
263
302
 
264
303
  - **Headless Runner**: spawns Claude Code processes with MCP bouncer integration, manages lifecycle
@@ -268,6 +307,8 @@ bin/
268
307
  - **PTY Manager**: terminal session management with tmux support and subscriber model
269
308
  - **Session Registry**: tab-to-session persistence across WebSocket disconnects
270
309
 
310
+ ---
311
+
271
312
  ## Optional Setup
272
313
 
273
314
  ### Web Terminal
@@ -321,6 +362,8 @@ npm uninstall -g mstro-app
321
362
  rm -rf ~/.mstro
322
363
  ```
323
364
 
365
+ ---
366
+
324
367
  ## Links
325
368
 
326
369
  - **Web App**: [mstro.app](https://mstro.app)
@@ -334,4 +377,4 @@ PolyForm Noncommercial License 1.0.0. See [LICENSE](./LICENSE).
334
377
 
335
378
  You can view, modify, and distribute the source code for any **noncommercial** purpose — personal study, hobby projects, research, evaluation, and use by charitable, educational, public-research, public-safety, environmental, or government organizations. Any commercial use, including offering a commercial product or service derived from this code, requires a separate commercial license from Mstro.
336
379
 
337
- For commercial licensing inquiries, contact hey@mstro.app.
380
+ For commercial licensing inquiries, contact bravo@mstro.app.
@@ -94,7 +94,7 @@ function showStatus() {
94
94
  log(' mstro telemetry on Enable telemetry', colors.dim);
95
95
  log(' mstro telemetry off Disable telemetry', colors.dim);
96
96
  log('');
97
- log(' Privacy policy: https://github.com/mstro-app/mstro/blob/main/cli/PRIVACY.md', colors.dim);
97
+ log(' Privacy policy: https://github.com/mstroapp/mstro/blob/main/cli/PRIVACY.md', colors.dim);
98
98
  log('');
99
99
  }
100
100
 
package/bin/mstro.js CHANGED
@@ -13,11 +13,12 @@
13
13
  * mstro whoami # Show current user
14
14
  * mstro status # Show connection status
15
15
  * mstro -p 4105 # Start on specific port (overrides auto port)
16
+ * mstro --detached # Start in background; survives terminal close
16
17
  * mstro --help # Show help
17
18
  */
18
19
 
19
20
  import { spawn } from 'node:child_process';
20
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
21
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
21
22
  import { homedir } from 'node:os';
22
23
  import { dirname, join, resolve } from 'node:path';
23
24
  import { createInterface } from 'node:readline';
@@ -294,6 +295,7 @@ function showHelp() {
294
295
  log(' mstro status Show connection and auth status', colors.dim);
295
296
  log(' mstro telemetry [on|off] Enable/disable anonymous telemetry', colors.dim);
296
297
  log(' mstro -p 4105 Start on specific port (overrides auto port)', colors.dim);
298
+ log(' mstro --detached Start in background, return prompt (survives terminal close)', colors.dim);
297
299
  log(' mstro setup-terminal Enable web terminal (compiles native module)', colors.dim);
298
300
  log(' mstro --version Show version number', colors.dim);
299
301
  log(' mstro --help Show this help message', colors.dim);
@@ -301,7 +303,7 @@ function showHelp() {
301
303
  log(' Options:', colors.bold);
302
304
  log(' --port, -p <port> Override automatic port selection', colors.dim);
303
305
  log(' --working-dir, -w <dir> Set working directory', colors.dim);
304
- log(' --staging Connect to the staging server', colors.dim);
306
+ log(' --detached Run in background and exit, leaving server alive', colors.dim);
305
307
  log(' --verbose, -v Enable verbose output', colors.dim);
306
308
  log('');
307
309
  log(' Authentication:', colors.bold);
@@ -310,6 +312,39 @@ function showHelp() {
310
312
  log('');
311
313
  }
312
314
 
315
+ function runNpmScriptDetached(script, args = [], envOverrides = {}) {
316
+ const logDir = join(homedir(), '.mstro', 'logs');
317
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
318
+ const logPath = join(logDir, `mstro-${new Date().toISOString().replace(/[:.]/g, '-')}.log`);
319
+ const logFd = openSync(logPath, 'a');
320
+
321
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
322
+ const child = spawn(npmCmd, ['run', '--silent', script, ...args], {
323
+ cwd: CLIENT_ROOT,
324
+ stdio: ['ignore', logFd, logFd],
325
+ env: { ...process.env, MSTRO_WORKING_DIR: USER_CWD, ...envOverrides },
326
+ detached: true,
327
+ ...(process.platform === 'win32' ? { windowsHide: true } : {}),
328
+ });
329
+
330
+ child.on('error', (err) => {
331
+ log(`Error: ${err.message}`, colors.red);
332
+ closeSync(logFd);
333
+ process.exit(1);
334
+ });
335
+
336
+ // Detach: parent exits, child keeps running independently of the terminal session.
337
+ child.unref();
338
+ closeSync(logFd);
339
+
340
+ log('\n Mstro started in background', colors.bold + colors.green);
341
+ log(` PID: ${child.pid}`, colors.dim);
342
+ log(` Log: ${logPath}`, colors.dim);
343
+ log(` Stop: kill ${child.pid}`, colors.dim);
344
+ log(' Status: mstro status\n', colors.dim);
345
+ process.exit(0);
346
+ }
347
+
313
348
  function runNpmScript(script, args = [], envOverrides = {}) {
314
349
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
315
350
  const child = spawn(npmCmd, ['run', '--silent', script, ...args], {
@@ -590,7 +625,22 @@ async function ensurePtySetup() {
590
625
  }
591
626
  }
592
627
 
593
- async function startServer(envOverrides) {
628
+ async function startServer(envOverrides, { detached = false } = {}) {
629
+ if (detached) {
630
+ if (!isLoggedIn()) {
631
+ log('\n Not logged in.', colors.red);
632
+ log(' Run "mstro login" first, then retry "mstro --detached".\n', colors.dim);
633
+ process.exit(1);
634
+ }
635
+ const ptyAvailable = await isNodePtyAvailable();
636
+ if (!ptyAvailable) {
637
+ log('\n Note: terminal support not enabled.', colors.yellow);
638
+ log(' Run "mstro setup-terminal" if you want the in-browser terminal.', colors.dim);
639
+ }
640
+ runNpmScriptDetached('start', [], envOverrides);
641
+ return;
642
+ }
643
+
594
644
  await ensureLoggedIn();
595
645
 
596
646
  await ensurePtySetup();
@@ -676,7 +726,8 @@ async function main() {
676
726
  }
677
727
 
678
728
  // Default: start server
679
- await startServer(envOverrides);
729
+ const detached = args.includes('--detached');
730
+ await startServer(envOverrides, { detached });
680
731
  }
681
732
 
682
733
  main();
@@ -0,0 +1,55 @@
1
+ export interface EtaBucket {
2
+ /** Elapsed-ms threshold for this bucket. */
3
+ elapsedMs: number;
4
+ /** Conditional p50 of TOTAL duration among movements still running at elapsedMs. */
5
+ p50TotalMs: number;
6
+ /** Conditional p90 of TOTAL duration. */
7
+ p90TotalMs: number;
8
+ /** Sample count behind this bucket. */
9
+ n: number;
10
+ }
11
+ export interface EtaProfile {
12
+ /** Buckets in ascending elapsedMs. */
13
+ buckets: EtaBucket[];
14
+ /** Number of movements the profile was built from. */
15
+ sampleSize: number;
16
+ /** ISO timestamp of when this profile was computed. */
17
+ computedAt: string;
18
+ }
19
+ export interface EtaPrediction {
20
+ /** Predicted total duration (p50). Always >= elapsed. */
21
+ p50TotalMs: number;
22
+ /** Predicted upper bound (p90). Always >= p50. */
23
+ p90TotalMs: number;
24
+ /** Sample size for the bucket used. */
25
+ n: number;
26
+ }
27
+ /**
28
+ * Build an EtaProfile from a `.mstro/history/` directory. Returns null if
29
+ * there isn't enough data to form a stable estimate.
30
+ */
31
+ export declare function buildEtaProfile(historyDir: string, opts?: {
32
+ maxFiles?: number;
33
+ }): Promise<EtaProfile | null>;
34
+ export declare function getEtaProfileCached(historyDir: string): Promise<EtaProfile | null>;
35
+ /** Test hook: clear the in-process cache. */
36
+ export declare function _clearEtaCache(): void;
37
+ /**
38
+ * Baseline profile shipped in the package so a fresh install (no
39
+ * `.mstro/history`) still gets a useful "typical" estimate from the very
40
+ * first prompt. Numbers below were computed offline from the largest
41
+ * available real-world history sample (mstro's own project, 379 movements
42
+ * spanning short Q&A through multi-hour autonomous runs); they reflect a
43
+ * heavy mix of chat, planning, and execution prompts. Once a project
44
+ * accumulates >= MIN_SAMPLES local movements its own profile takes over.
45
+ */
46
+ export declare const BASELINE_ETA_PROFILE: EtaProfile;
47
+ /** Synchronously build a profile from an in-memory list of durationMs values. Exposed for tests. */
48
+ export declare function buildProfileFromDurations(durationsMs: number[]): EtaProfile;
49
+ /**
50
+ * Predict total duration given current elapsed ms. Returns null if the
51
+ * profile has no usable buckets. The returned p50 is clamped to elapsed (so
52
+ * the indicator never shows a typical that has already passed).
53
+ */
54
+ export declare function predictEta(profile: EtaProfile, elapsedMs: number): EtaPrediction | null;
55
+ //# sourceMappingURL=eta-estimator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eta-estimator.d.ts","sourceRoot":"","sources":["../../../server/cli/eta-estimator.ts"],"names":[],"mappings":"AAyDA,MAAM,WAAW,SAAS;IACxB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,UAAU,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,UAAU;IACzB,sCAAsC;IACtC,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/B,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAK5B;AAeD,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAcxF;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,IAAI,IAAI,CAA0B;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,EAAE,UAclC,CAAC;AAEF,oGAAoG;AACpG,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,UAAU,CAoB3E;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAavF"}
@@ -0,0 +1,222 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ /**
3
+ * ETA estimator for the chat composing indicator.
4
+ *
5
+ * Reads recent movements from `.mstro/history/*.json` and builds a small
6
+ * conditional-quantile table: for each elapsed-time checkpoint, the p50/p90
7
+ * of TOTAL movement duration among movements that hadn't finished yet at
8
+ * that elapsed time. The web indicator interpolates against this table to
9
+ * render "Composing · {elapsed} · ~{p50} typical · {tokens}".
10
+ *
11
+ * Why conditional-on-elapsed and not a regression on prompt features:
12
+ * - prompt length is uncorrelated with duration (r≈0.05); tool count is
13
+ * strong (r≈0.74) but unknown a priori. Conditioning on elapsed alone
14
+ * beats a static estimate dramatically — accuracy at 5m elapsed is
15
+ * ~38% MAPE vs 160% at 0s with the same lookup, because the longer the
16
+ * run goes, the smaller the cohort it could still belong to.
17
+ *
18
+ * Why a quantile table and not a regression model:
19
+ * - The duration distribution is heavily skewed (mean 4m20s, median 1m49s,
20
+ * p99 29m). A point estimate from a regression would be misleading; the
21
+ * web shows a typical/range pair so users see "around X, can be up to Y".
22
+ *
23
+ * Sample selection:
24
+ * - Up to MAX_SAMPLE_FILES most recent files by mtime, keeping work bounded
25
+ * and biasing toward recent behavior. Movements with durationMs < 1s or
26
+ * above SANITY_CEILING_MS are dropped as outliers (cancelled before they
27
+ * started, or runaway sessions that don't represent typical waits).
28
+ *
29
+ * Returns `null` when there are fewer than MIN_SAMPLES movements; the caller
30
+ * falls back to "no ETA" rather than inventing one from too little data.
31
+ */
32
+ import { promises as fsp } from 'node:fs';
33
+ import { join } from 'node:path';
34
+ /** Bucket boundaries (ms) at which we precompute conditional quantiles. */
35
+ const ELAPSED_CHECKPOINTS_MS = [
36
+ 0, // a-priori (elapsed=0)
37
+ 10_000, // 10s
38
+ 30_000, // 30s
39
+ 60_000, // 1m
40
+ 120_000, // 2m
41
+ 300_000, // 5m
42
+ 600_000, // 10m
43
+ 900_000, // 15m
44
+ 1_500_000, // 25m
45
+ 2_400_000, // 40m
46
+ 3_600_000, // 60m
47
+ ];
48
+ const MAX_SAMPLE_FILES = 200;
49
+ const MIN_SAMPLES = 30;
50
+ const SANITY_FLOOR_MS = 1_000; // <1s = noise (errors, instant cancels)
51
+ const SANITY_CEILING_MS = 6 * 60 * 60_000; // 6h cap
52
+ /**
53
+ * Build an EtaProfile from a `.mstro/history/` directory. Returns null if
54
+ * there isn't enough data to form a stable estimate.
55
+ */
56
+ export async function buildEtaProfile(historyDir, opts = {}) {
57
+ const maxFiles = opts.maxFiles ?? MAX_SAMPLE_FILES;
58
+ const durations = await collectRecentDurations(historyDir, maxFiles);
59
+ if (durations.length < MIN_SAMPLES)
60
+ return null;
61
+ return buildProfileFromDurations(durations);
62
+ }
63
+ /**
64
+ * Cached variant for the WebSocket flow: same project's many tabs ask for
65
+ * the same profile within minutes of each other, and rescanning 200 files
66
+ * each time wastes I/O. Cache by historyDir with a TTL so that fresh
67
+ * movements eventually feed back into the estimate.
68
+ *
69
+ * Falls back to BASELINE_ETA_PROFILE when the local history is too thin —
70
+ * new installs still get a sensible "Composing · Xs / ~Ys" indicator from
71
+ * prompt 1 instead of waiting for 30+ runs to accumulate.
72
+ */
73
+ const PROFILE_CACHE_TTL_MS = 5 * 60_000; // 5 minutes
74
+ const profileCache = new Map();
75
+ export async function getEtaProfileCached(historyDir) {
76
+ const now = Date.now();
77
+ const hit = profileCache.get(historyDir);
78
+ if (hit && hit.expiresAt > now)
79
+ return hit.profile ?? BASELINE_ETA_PROFILE;
80
+ if (hit?.pending)
81
+ return hit.pending;
82
+ const pending = buildEtaProfile(historyDir).then(profile => {
83
+ profileCache.set(historyDir, { profile, expiresAt: Date.now() + PROFILE_CACHE_TTL_MS });
84
+ return profile ?? BASELINE_ETA_PROFILE;
85
+ }).catch(() => {
86
+ profileCache.set(historyDir, { profile: null, expiresAt: Date.now() + PROFILE_CACHE_TTL_MS });
87
+ return BASELINE_ETA_PROFILE;
88
+ });
89
+ profileCache.set(historyDir, { profile: hit?.profile ?? null, expiresAt: hit?.expiresAt ?? 0, pending });
90
+ return pending;
91
+ }
92
+ /** Test hook: clear the in-process cache. */
93
+ export function _clearEtaCache() { profileCache.clear(); }
94
+ /**
95
+ * Baseline profile shipped in the package so a fresh install (no
96
+ * `.mstro/history`) still gets a useful "typical" estimate from the very
97
+ * first prompt. Numbers below were computed offline from the largest
98
+ * available real-world history sample (mstro's own project, 379 movements
99
+ * spanning short Q&A through multi-hour autonomous runs); they reflect a
100
+ * heavy mix of chat, planning, and execution prompts. Once a project
101
+ * accumulates >= MIN_SAMPLES local movements its own profile takes over.
102
+ */
103
+ export const BASELINE_ETA_PROFILE = {
104
+ buckets: [
105
+ { elapsedMs: 0, p50TotalMs: 108_000, p90TotalMs: 768_000, n: 379 },
106
+ { elapsedMs: 10_000, p50TotalMs: 117_000, p90TotalMs: 769_000, n: 368 },
107
+ { elapsedMs: 30_000, p50TotalMs: 155_000, p90TotalMs: 860_000, n: 328 },
108
+ { elapsedMs: 60_000, p50TotalMs: 245_000, p90TotalMs: 1_013_000, n: 252 },
109
+ { elapsedMs: 120_000, p50TotalMs: 392_000, p90TotalMs: 1_171_000, n: 182 },
110
+ { elapsedMs: 300_000, p50TotalMs: 605_000, p90TotalMs: 1_412_000, n: 116 },
111
+ { elapsedMs: 600_000, p50TotalMs: 945_000, p90TotalMs: 1_679_000, n: 58 },
112
+ { elapsedMs: 900_000, p50TotalMs: 1_265_000, p90TotalMs: 1_845_000, n: 30 },
113
+ { elapsedMs: 1_500_000, p50TotalMs: 1_728_000, p90TotalMs: 1_986_000, n: 10 },
114
+ ],
115
+ sampleSize: 379,
116
+ computedAt: '2026-05-06T00:00:00.000Z',
117
+ };
118
+ /** Synchronously build a profile from an in-memory list of durationMs values. Exposed for tests. */
119
+ export function buildProfileFromDurations(durationsMs) {
120
+ const cleaned = durationsMs
121
+ .filter(d => Number.isFinite(d) && d >= SANITY_FLOOR_MS && d <= SANITY_CEILING_MS)
122
+ .sort((a, b) => a - b);
123
+ const buckets = [];
124
+ for (const elapsedMs of ELAPSED_CHECKPOINTS_MS) {
125
+ const stillRunning = cleaned.filter(d => d > elapsedMs);
126
+ if (stillRunning.length === 0)
127
+ break;
128
+ buckets.push({
129
+ elapsedMs,
130
+ p50TotalMs: quantile(stillRunning, 0.5),
131
+ p90TotalMs: quantile(stillRunning, 0.9),
132
+ n: stillRunning.length,
133
+ });
134
+ }
135
+ return {
136
+ buckets,
137
+ sampleSize: cleaned.length,
138
+ computedAt: new Date().toISOString(),
139
+ };
140
+ }
141
+ /**
142
+ * Predict total duration given current elapsed ms. Returns null if the
143
+ * profile has no usable buckets. The returned p50 is clamped to elapsed (so
144
+ * the indicator never shows a typical that has already passed).
145
+ */
146
+ export function predictEta(profile, elapsedMs) {
147
+ if (profile.buckets.length === 0)
148
+ return null;
149
+ let bucket = profile.buckets[0];
150
+ for (const b of profile.buckets) {
151
+ if (b.elapsedMs <= elapsedMs)
152
+ bucket = b;
153
+ else
154
+ break;
155
+ }
156
+ // If elapsed has surpassed the last bucket's p50, the run is in the long
157
+ // tail. Keep the last bucket's quantiles but never report a "typical" that
158
+ // is shorter than elapsed itself — that would be nonsensical UX.
159
+ const p50TotalMs = Math.max(bucket.p50TotalMs, elapsedMs);
160
+ const p90TotalMs = Math.max(bucket.p90TotalMs, p50TotalMs);
161
+ return { p50TotalMs, p90TotalMs, n: bucket.n };
162
+ }
163
+ // -- internals --
164
+ async function collectRecentDurations(historyDir, maxFiles) {
165
+ let entries;
166
+ try {
167
+ entries = (await fsp.readdir(historyDir)).filter(f => f.endsWith('.json'));
168
+ }
169
+ catch {
170
+ return [];
171
+ }
172
+ if (entries.length === 0)
173
+ return [];
174
+ // Sort by mtime DESC for recency. statting up to N files is acceptable —
175
+ // even a few thousand stats is sub-100ms on local disk.
176
+ const stats = await Promise.all(entries.map(async (name) => {
177
+ try {
178
+ const full = join(historyDir, name);
179
+ const s = await fsp.stat(full);
180
+ return { full, mtime: s.mtimeMs };
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ }));
186
+ const ordered = stats
187
+ .filter((x) => x !== null)
188
+ .sort((a, b) => b.mtime - a.mtime)
189
+ .slice(0, maxFiles);
190
+ const durations = [];
191
+ for (const { full } of ordered) {
192
+ let raw;
193
+ try {
194
+ raw = await fsp.readFile(full, 'utf-8');
195
+ }
196
+ catch {
197
+ continue;
198
+ }
199
+ let data;
200
+ try {
201
+ data = JSON.parse(raw);
202
+ }
203
+ catch {
204
+ continue;
205
+ }
206
+ if (!Array.isArray(data.movements))
207
+ continue;
208
+ for (const m of data.movements) {
209
+ const d = m.durationMs;
210
+ if (typeof d === 'number' && Number.isFinite(d))
211
+ durations.push(d);
212
+ }
213
+ }
214
+ return durations;
215
+ }
216
+ function quantile(sortedAsc, q) {
217
+ if (sortedAsc.length === 0)
218
+ return 0;
219
+ const idx = Math.min(sortedAsc.length - 1, Math.floor(sortedAsc.length * q));
220
+ return sortedAsc[idx];
221
+ }
222
+ //# sourceMappingURL=eta-estimator.js.map