mstro-app 0.5.1 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRIVACY.md +9 -9
- package/README.md +71 -28
- package/bin/commands/config.js +1 -1
- package/bin/mstro.js +55 -4
- package/dist/server/cli/eta-estimator.d.ts +55 -0
- package/dist/server/cli/eta-estimator.d.ts.map +1 -0
- package/dist/server/cli/eta-estimator.js +222 -0
- package/dist/server/cli/eta-estimator.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +64 -9
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +19 -12
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
- package/dist/server/cli/improvisation-history-store.js +5 -1
- package/dist/server/cli/improvisation-history-store.js.map +1 -1
- package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
- package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
- package/dist/server/cli/improvisation-output-queue.js +30 -7
- package/dist/server/cli/improvisation-output-queue.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +29 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +50 -1
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +2 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/engines/EngineEvent.d.ts +126 -0
- package/dist/server/engines/EngineEvent.d.ts.map +1 -0
- package/dist/server/engines/EngineEvent.js +11 -0
- package/dist/server/engines/EngineEvent.js.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
- package/dist/server/engines/factory.d.ts +21 -0
- package/dist/server/engines/factory.d.ts.map +1 -0
- package/dist/server/engines/factory.js +152 -0
- package/dist/server/engines/factory.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
- package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
- package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
- package/dist/server/engines/opencode/model-catalog.js +141 -0
- package/dist/server/engines/opencode/model-catalog.js.map +1 -0
- package/dist/server/engines/types.d.ts +146 -0
- package/dist/server/engines/types.d.ts.map +1 -0
- package/dist/server/engines/types.js +4 -0
- package/dist/server/engines/types.js.map +1 -0
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +8 -124
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +45 -0
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +69 -5
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/factory.d.ts +70 -0
- package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
- package/dist/server/mcp/classifier/factory.js +155 -0
- package/dist/server/mcp/classifier/factory.js.map +1 -0
- package/dist/server/services/plan/agent-resolver.d.ts +26 -0
- package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
- package/dist/server/services/plan/agent-resolver.js +102 -0
- package/dist/server/services/plan/agent-resolver.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +59 -11
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +3 -1
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +33 -1
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/settings.d.ts +76 -2
- package/dist/server/services/settings.d.ts.map +1 -1
- package/dist/server/services/settings.js +127 -4
- package/dist/server/services/settings.js.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.js +19 -6
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +17 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +54 -2
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-complexity.js +78 -26
- package/dist/server/services/websocket/quality-complexity.js.map +1 -1
- package/dist/server/services/websocket/quality-eta.d.ts +47 -0
- package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-eta.js +110 -0
- package/dist/server/services/websocket/quality-eta.js.map +1 -0
- package/dist/server/services/websocket/quality-grading.d.ts +27 -4
- package/dist/server/services/websocket/quality-grading.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-grading.js +369 -201
- package/dist/server/services/websocket/quality-grading.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +145 -7
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-operations.d.ts +34 -0
- package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-operations.js +47 -0
- package/dist/server/services/websocket/quality-operations.js.map +1 -0
- package/dist/server/services/websocket/quality-persistence.d.ts +9 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +10 -0
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +105 -56
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +9 -1
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +334 -14
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +21 -0
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +49 -0
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +35 -2
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js +1 -1
- package/dist/server/services/websocket/quality-types.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +3 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +57 -9
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.js +3 -0
- package/dist/server/services/websocket/session-history.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +158 -42
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +25 -0
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +19 -0
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/settings-handlers.js +35 -4
- package/dist/server/services/websocket/settings-handlers.js.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
- package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.js +10 -2
- package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
- package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.js +138 -12
- package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
- package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.js +55 -2
- package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +47 -2
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +28 -5
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +10 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +5 -3
- package/server/cli/eta-estimator.ts +249 -0
- package/server/cli/headless/stall-assessor.ts +93 -0
- package/server/cli/headless/tool-watchdog.ts +21 -0
- package/server/cli/improvisation-history-store.ts +4 -1
- package/server/cli/improvisation-output-queue.ts +29 -7
- package/server/cli/improvisation-session-manager.ts +54 -1
- package/server/cli/improvisation-types.ts +2 -0
- package/server/engines/EngineEvent.ts +156 -0
- package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
- package/server/engines/factory.ts +176 -0
- package/server/engines/opencode/OpenCodeEngine.ts +786 -0
- package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
- package/server/engines/opencode/model-catalog.ts +217 -0
- package/server/engines/types.ts +173 -0
- package/server/index.ts +1 -1
- package/server/mcp/bouncer-haiku.ts +21 -145
- package/server/mcp/bouncer-integration.ts +107 -5
- package/server/mcp/classifier/BouncerClassifier.ts +40 -0
- package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
- package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
- package/server/mcp/classifier/factory.ts +195 -0
- package/server/services/plan/agent-resolver.ts +115 -0
- package/server/services/plan/agents/code-review.md +38 -8
- package/server/services/plan/composer.ts +63 -11
- package/server/services/plan/executor.ts +3 -1
- package/server/services/plan/issue-prompt-builder.ts +39 -1
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/types.ts +4 -0
- package/server/services/settings.ts +161 -4
- package/server/services/websocket/git-branch-handlers.ts +20 -6
- package/server/services/websocket/handler.ts +59 -2
- package/server/services/websocket/quality-complexity.ts +80 -26
- package/server/services/websocket/quality-eta.ts +155 -0
- package/server/services/websocket/quality-grading.ts +445 -222
- package/server/services/websocket/quality-handlers.ts +153 -7
- package/server/services/websocket/quality-operations.ts +72 -0
- package/server/services/websocket/quality-persistence.ts +17 -0
- package/server/services/websocket/quality-review-agent.ts +154 -64
- package/server/services/websocket/quality-service.ts +361 -13
- package/server/services/websocket/quality-tools.ts +51 -0
- package/server/services/websocket/quality-types.ts +41 -2
- package/server/services/websocket/session-handlers.ts +64 -10
- package/server/services/websocket/session-history.ts +3 -0
- package/server/services/websocket/session-initialization.ts +189 -46
- package/server/services/websocket/session-registry.ts +37 -0
- package/server/services/websocket/settings-handlers.ts +41 -4
- package/server/services/websocket/tab-broadcast.ts +10 -2
- package/server/services/websocket/tab-event-buffer.ts +143 -11
- package/server/services/websocket/tab-event-replay.ts +70 -3
- package/server/services/websocket/tab-handlers.ts +53 -5
- package/server/services/websocket/types.ts +37 -5
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
|
|
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
|
|
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
|
|
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
|
|
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:**
|
|
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
|
|
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/
|
|
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:**
|
|
354
|
+
**Email:** bravo@mstro.app
|
|
355
355
|
**Website:** https://mstro.app
|
|
356
|
-
**GitHub Issues:** https://github.com/
|
|
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
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="https://mstro.app/mstro-icon.svg" alt="Mstro" width="96" height="96" />
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# Mstro
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/mstro-app)
|
|
10
|
+
[](https://www.npmjs.com/package/mstro-app)
|
|
11
|
+
[](https://nodejs.org/)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
[](https://docs.anthropic.com/en/docs/claude-code)
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
[Website](https://mstro.app) · [Blog](https://mstro.app/blog) · [Compare](https://mstro.app/compare) · [Security](./SECURITY.md)
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
</div>
|
|
22
18
|
|
|
23
|
-
|
|
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
|
-
|
|
23
|
+
---
|
|
26
24
|
|
|
27
|
-
|
|
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
|
-
|
|
27
|
+
> **Free for the first 1,000 users.** No credit card. Bring your own Anthropic API key.
|
|
30
28
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
380
|
+
For commercial licensing inquiries, contact bravo@mstro.app.
|
package/bin/commands/config.js
CHANGED
|
@@ -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/
|
|
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(' --
|
|
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
|
-
|
|
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
|