mstro-app 0.1.47
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/LICENSE +21 -0
- package/README.md +177 -0
- package/bin/commands/config.js +145 -0
- package/bin/commands/login.js +313 -0
- package/bin/commands/logout.js +75 -0
- package/bin/commands/status.js +197 -0
- package/bin/commands/whoami.js +161 -0
- package/bin/configure-claude.js +298 -0
- package/bin/mstro.js +581 -0
- package/bin/postinstall.js +45 -0
- package/bin/release.sh +110 -0
- package/dist/server/cli/headless/claude-invoker.d.ts +17 -0
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker.js +311 -0
- package/dist/server/cli/headless/claude-invoker.js.map +1 -0
- package/dist/server/cli/headless/index.d.ts +13 -0
- package/dist/server/cli/headless/index.d.ts.map +1 -0
- package/dist/server/cli/headless/index.js +10 -0
- package/dist/server/cli/headless/index.js.map +1 -0
- package/dist/server/cli/headless/mcp-config.d.ts +11 -0
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -0
- package/dist/server/cli/headless/mcp-config.js +76 -0
- package/dist/server/cli/headless/mcp-config.js.map +1 -0
- package/dist/server/cli/headless/output-utils.d.ts +33 -0
- package/dist/server/cli/headless/output-utils.d.ts.map +1 -0
- package/dist/server/cli/headless/output-utils.js +101 -0
- package/dist/server/cli/headless/output-utils.js.map +1 -0
- package/dist/server/cli/headless/prompt-utils.d.ts +21 -0
- package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -0
- package/dist/server/cli/headless/prompt-utils.js +84 -0
- package/dist/server/cli/headless/prompt-utils.js.map +1 -0
- package/dist/server/cli/headless/runner.d.ts +24 -0
- package/dist/server/cli/headless/runner.d.ts.map +1 -0
- package/dist/server/cli/headless/runner.js +99 -0
- package/dist/server/cli/headless/runner.js.map +1 -0
- package/dist/server/cli/headless/types.d.ts +106 -0
- package/dist/server/cli/headless/types.d.ts.map +1 -0
- package/dist/server/cli/headless/types.js +4 -0
- package/dist/server/cli/headless/types.js.map +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts +155 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -0
- package/dist/server/cli/improvisation-session-manager.js +415 -0
- package/dist/server/cli/improvisation-session-manager.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +386 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/mcp/bouncer-cli.d.ts +3 -0
- package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-cli.js +99 -0
- package/dist/server/mcp/bouncer-cli.js.map +1 -0
- package/dist/server/mcp/bouncer-integration.d.ts +36 -0
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-integration.js +301 -0
- package/dist/server/mcp/bouncer-integration.js.map +1 -0
- package/dist/server/mcp/security-audit.d.ts +52 -0
- package/dist/server/mcp/security-audit.d.ts.map +1 -0
- package/dist/server/mcp/security-audit.js +118 -0
- package/dist/server/mcp/security-audit.js.map +1 -0
- package/dist/server/mcp/security-patterns.d.ts +73 -0
- package/dist/server/mcp/security-patterns.d.ts.map +1 -0
- package/dist/server/mcp/security-patterns.js +247 -0
- package/dist/server/mcp/security-patterns.js.map +1 -0
- package/dist/server/mcp/server.d.ts +3 -0
- package/dist/server/mcp/server.d.ts.map +1 -0
- package/dist/server/mcp/server.js +146 -0
- package/dist/server/mcp/server.js.map +1 -0
- package/dist/server/routes/files.d.ts +9 -0
- package/dist/server/routes/files.d.ts.map +1 -0
- package/dist/server/routes/files.js +24 -0
- package/dist/server/routes/files.js.map +1 -0
- package/dist/server/routes/improvise.d.ts +3 -0
- package/dist/server/routes/improvise.d.ts.map +1 -0
- package/dist/server/routes/improvise.js +72 -0
- package/dist/server/routes/improvise.js.map +1 -0
- package/dist/server/routes/index.d.ts +10 -0
- package/dist/server/routes/index.d.ts.map +1 -0
- package/dist/server/routes/index.js +12 -0
- package/dist/server/routes/index.js.map +1 -0
- package/dist/server/routes/instances.d.ts +10 -0
- package/dist/server/routes/instances.d.ts.map +1 -0
- package/dist/server/routes/instances.js +47 -0
- package/dist/server/routes/instances.js.map +1 -0
- package/dist/server/routes/notifications.d.ts +3 -0
- package/dist/server/routes/notifications.d.ts.map +1 -0
- package/dist/server/routes/notifications.js +136 -0
- package/dist/server/routes/notifications.js.map +1 -0
- package/dist/server/services/analytics.d.ts +56 -0
- package/dist/server/services/analytics.d.ts.map +1 -0
- package/dist/server/services/analytics.js +240 -0
- package/dist/server/services/analytics.js.map +1 -0
- package/dist/server/services/auth.d.ts +26 -0
- package/dist/server/services/auth.d.ts.map +1 -0
- package/dist/server/services/auth.js +71 -0
- package/dist/server/services/auth.js.map +1 -0
- package/dist/server/services/client-id.d.ts +10 -0
- package/dist/server/services/client-id.d.ts.map +1 -0
- package/dist/server/services/client-id.js +61 -0
- package/dist/server/services/client-id.js.map +1 -0
- package/dist/server/services/credentials.d.ts +39 -0
- package/dist/server/services/credentials.d.ts.map +1 -0
- package/dist/server/services/credentials.js +110 -0
- package/dist/server/services/credentials.js.map +1 -0
- package/dist/server/services/files.d.ts +119 -0
- package/dist/server/services/files.d.ts.map +1 -0
- package/dist/server/services/files.js +560 -0
- package/dist/server/services/files.js.map +1 -0
- package/dist/server/services/instances.d.ts +52 -0
- package/dist/server/services/instances.d.ts.map +1 -0
- package/dist/server/services/instances.js +241 -0
- package/dist/server/services/instances.js.map +1 -0
- package/dist/server/services/pathUtils.d.ts +47 -0
- package/dist/server/services/pathUtils.d.ts.map +1 -0
- package/dist/server/services/pathUtils.js +124 -0
- package/dist/server/services/pathUtils.js.map +1 -0
- package/dist/server/services/platform.d.ts +72 -0
- package/dist/server/services/platform.d.ts.map +1 -0
- package/dist/server/services/platform.js +368 -0
- package/dist/server/services/platform.js.map +1 -0
- package/dist/server/services/sentry.d.ts +5 -0
- package/dist/server/services/sentry.d.ts.map +1 -0
- package/dist/server/services/sentry.js +71 -0
- package/dist/server/services/sentry.js.map +1 -0
- package/dist/server/services/terminal/pty-manager.d.ts +149 -0
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -0
- package/dist/server/services/terminal/pty-manager.js +377 -0
- package/dist/server/services/terminal/pty-manager.js.map +1 -0
- package/dist/server/services/terminal/tmux-manager.d.ts +82 -0
- package/dist/server/services/terminal/tmux-manager.d.ts.map +1 -0
- package/dist/server/services/terminal/tmux-manager.js +352 -0
- package/dist/server/services/terminal/tmux-manager.js.map +1 -0
- package/dist/server/services/websocket/autocomplete.d.ts +50 -0
- package/dist/server/services/websocket/autocomplete.d.ts.map +1 -0
- package/dist/server/services/websocket/autocomplete.js +361 -0
- package/dist/server/services/websocket/autocomplete.js.map +1 -0
- package/dist/server/services/websocket/file-utils.d.ts +44 -0
- package/dist/server/services/websocket/file-utils.d.ts.map +1 -0
- package/dist/server/services/websocket/file-utils.js +272 -0
- package/dist/server/services/websocket/file-utils.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +246 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -0
- package/dist/server/services/websocket/handler.js +1771 -0
- package/dist/server/services/websocket/handler.js.map +1 -0
- package/dist/server/services/websocket/index.d.ts +11 -0
- package/dist/server/services/websocket/index.d.ts.map +1 -0
- package/dist/server/services/websocket/index.js +14 -0
- package/dist/server/services/websocket/index.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +214 -0
- package/dist/server/services/websocket/types.d.ts.map +1 -0
- package/dist/server/services/websocket/types.js +4 -0
- package/dist/server/services/websocket/types.js.map +1 -0
- package/dist/server/utils/agent-manager.d.ts +69 -0
- package/dist/server/utils/agent-manager.d.ts.map +1 -0
- package/dist/server/utils/agent-manager.js +269 -0
- package/dist/server/utils/agent-manager.js.map +1 -0
- package/dist/server/utils/paths.d.ts +25 -0
- package/dist/server/utils/paths.d.ts.map +1 -0
- package/dist/server/utils/paths.js +38 -0
- package/dist/server/utils/paths.js.map +1 -0
- package/dist/server/utils/port-manager.d.ts +10 -0
- package/dist/server/utils/port-manager.d.ts.map +1 -0
- package/dist/server/utils/port-manager.js +60 -0
- package/dist/server/utils/port-manager.js.map +1 -0
- package/dist/server/utils/port.d.ts +26 -0
- package/dist/server/utils/port.d.ts.map +1 -0
- package/dist/server/utils/port.js +83 -0
- package/dist/server/utils/port.js.map +1 -0
- package/hooks/bouncer.sh +138 -0
- package/package.json +74 -0
- package/server/README.md +191 -0
- package/server/cli/headless/claude-invoker.ts +415 -0
- package/server/cli/headless/index.ts +39 -0
- package/server/cli/headless/mcp-config.ts +87 -0
- package/server/cli/headless/output-utils.ts +109 -0
- package/server/cli/headless/prompt-utils.ts +108 -0
- package/server/cli/headless/runner.ts +133 -0
- package/server/cli/headless/types.ts +118 -0
- package/server/cli/improvisation-session-manager.ts +531 -0
- package/server/index.ts +456 -0
- package/server/mcp/README.md +122 -0
- package/server/mcp/bouncer-cli.ts +127 -0
- package/server/mcp/bouncer-integration.ts +430 -0
- package/server/mcp/security-audit.ts +180 -0
- package/server/mcp/security-patterns.ts +290 -0
- package/server/mcp/server.ts +174 -0
- package/server/routes/files.ts +29 -0
- package/server/routes/improvise.ts +82 -0
- package/server/routes/index.ts +13 -0
- package/server/routes/instances.ts +54 -0
- package/server/routes/notifications.ts +158 -0
- package/server/services/analytics.ts +277 -0
- package/server/services/auth.ts +80 -0
- package/server/services/client-id.ts +68 -0
- package/server/services/credentials.ts +134 -0
- package/server/services/files.ts +710 -0
- package/server/services/instances.ts +275 -0
- package/server/services/pathUtils.ts +158 -0
- package/server/services/platform.test.ts +1314 -0
- package/server/services/platform.ts +435 -0
- package/server/services/sentry.ts +81 -0
- package/server/services/terminal/pty-manager.ts +464 -0
- package/server/services/terminal/tmux-manager.ts +426 -0
- package/server/services/websocket/autocomplete.ts +438 -0
- package/server/services/websocket/file-utils.ts +305 -0
- package/server/services/websocket/handler.test.ts +20 -0
- package/server/services/websocket/handler.ts +2047 -0
- package/server/services/websocket/index.ts +40 -0
- package/server/services/websocket/types.ts +339 -0
- package/server/tsconfig.json +19 -0
- package/server/utils/agent-manager.ts +323 -0
- package/server/utils/paths.ts +45 -0
- package/server/utils/port-manager.ts +70 -0
- package/server/utils/port.ts +102 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Mstro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# mstro
|
|
2
|
+
|
|
3
|
+
Luxurious remote interface for [Claude Code](https://docs.anthropic.com/en/docs/claude-code). Run AI-powered coding sessions from any browser while Claude executes locally on any of your machines.
|
|
4
|
+
|
|
5
|
+
**mstro** is the CLI client for [mstro.app](https://mstro.app). It runs on your machine (laptop, cloud VM, CI server) and connects to the mstro.app web interface via a secure relay. You write prompts in the browser, Claude Code runs in your terminal.
|
|
6
|
+
|
|
7
|
+
**Get started at [mstro.app](https://mstro.app)** — create an account, then install this CLI to connect your machine.
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Browser (mstro.app) <--WebSocket--> Platform Server (relay) <--WebSocket--> mstro (your machine)
|
|
13
|
+
|
|
|
14
|
+
Claude Code CLI
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
1. `mstro` starts a local server and connects to the mstro.app platform server
|
|
18
|
+
2. You open [mstro.app](https://mstro.app) in any browser and see your connected machine
|
|
19
|
+
3. Prompts you send in the browser are relayed to your machine
|
|
20
|
+
4. Claude Code runs locally with full access to your project files
|
|
21
|
+
5. Output streams back to the browser in real-time
|
|
22
|
+
|
|
23
|
+
Run Claude Code on a powerful remote machine and interact with it from your phone, tablet, or any device with a browser.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g mstro
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated (`claude` CLI available in your PATH).
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
mstro login # Authenticate this device with your mstro.app account
|
|
37
|
+
mstro # Start mstro in your project directory
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
On first run, mstro will offer to set up the **Security Bouncer** - a tool permission manager that protects against dangerous operations. Say yes.
|
|
41
|
+
|
|
42
|
+
Then open [mstro.app](https://mstro.app) in your browser. Your machine appears as a connected "orchestra." Start prompting.
|
|
43
|
+
|
|
44
|
+
## Security Bouncer
|
|
45
|
+
|
|
46
|
+
The Bouncer replaces the default human-in-the-loop approval model with an agent-in-the-loop approach. An AI reviewer is better suited to evaluate tool calls than a human — it has full context on what should and shouldn't run, responds in milliseconds instead of interrupting your flow, and frees you up to focus on higher-level work while Claude Code executes autonomously. The result is faster, safer workflows without the constant approval prompts.
|
|
47
|
+
|
|
48
|
+
The bouncer hook is installed globally at `~/.claude/hooks/bouncer.sh` and applies to all Claude Code sessions, but the level of protection depends on how Claude Code is running:
|
|
49
|
+
|
|
50
|
+
**Mstro sessions (headless)** get the full 2-layer system:
|
|
51
|
+
|
|
52
|
+
1. **Pattern matching** (<5ms): Known-safe operations are allowed instantly. Known-dangerous patterns (destructive commands, fork bombs) are blocked instantly.
|
|
53
|
+
2. **AI analysis** (~200-500ms): Ambiguous operations are reviewed by a fast AI model to determine if they look like legitimate development work or prompt injection.
|
|
54
|
+
|
|
55
|
+
**Claude Code terminal REPL** (`claude`) gets 1-layer protection:
|
|
56
|
+
|
|
57
|
+
1. **Pattern matching only**: Blocks critical threats (fork bombs, `rm -rf /`, disk overwrites). Allows everything else. The AI analysis layer requires a running mstro server.
|
|
58
|
+
|
|
59
|
+
### Configure
|
|
60
|
+
|
|
61
|
+
The bouncer is set up automatically on first run. To reconfigure or install manually:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
mstro configure-hooks
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This installs a hook at `~/.claude/hooks/bouncer.sh` and registers it in `~/.claude/settings.json`.
|
|
68
|
+
|
|
69
|
+
Set `BOUNCER_USE_AI=false` to disable the AI analysis layer (pattern matching only).
|
|
70
|
+
|
|
71
|
+
## CLI Reference
|
|
72
|
+
|
|
73
|
+
### Commands
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
mstro # Start the client server
|
|
77
|
+
mstro login # Authenticate this device with mstro.app
|
|
78
|
+
mstro logout # Sign out
|
|
79
|
+
mstro whoami # Show current user and device info
|
|
80
|
+
mstro status # Show connection and auth status
|
|
81
|
+
mstro setup-terminal # Enable web terminal (compiles native module)
|
|
82
|
+
mstro configure-hooks # Install/reconfigure Security Bouncer
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Options
|
|
86
|
+
|
|
87
|
+
| Option | Description |
|
|
88
|
+
|--------|-------------|
|
|
89
|
+
| `-p, --port <port>` | Start on a specific port (default: 4101, auto-increments if busy) |
|
|
90
|
+
| `-w, --working-dir <dir>` | Set working directory |
|
|
91
|
+
| `-v, --verbose` | Verbose output |
|
|
92
|
+
| `--dev` | Connect to local platform at localhost:4102 |
|
|
93
|
+
| `--version` | Show version |
|
|
94
|
+
| `--help` | Show help |
|
|
95
|
+
|
|
96
|
+
## Multiple Instances
|
|
97
|
+
|
|
98
|
+
Run multiple mstro instances for different projects. Each auto-selects an available port:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
$ mstro # Project A → port 4101
|
|
102
|
+
$ mstro # Project B → port 4102
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Each instance appears as a separate orchestra in the web interface.
|
|
106
|
+
|
|
107
|
+
## Environment Variables
|
|
108
|
+
|
|
109
|
+
| Variable | Description |
|
|
110
|
+
|----------|-------------|
|
|
111
|
+
| `PORT` | Override server port |
|
|
112
|
+
| `BOUNCER_USE_AI` | Set to `false` to disable AI analysis layer |
|
|
113
|
+
| `PLATFORM_URL` | Platform server URL (default: `https://api.mstro.app`) |
|
|
114
|
+
|
|
115
|
+
## Config Files
|
|
116
|
+
|
|
117
|
+
mstro stores config in `~/.mstro/`:
|
|
118
|
+
|
|
119
|
+
| File | Purpose |
|
|
120
|
+
|------|---------|
|
|
121
|
+
| `~/.mstro/credentials.json` | Device auth token (created by `mstro login`) |
|
|
122
|
+
| `~/.claude/hooks/bouncer.sh` | Security Bouncer hook |
|
|
123
|
+
| `~/.claude/logs/bouncer.log` | Bouncer audit log |
|
|
124
|
+
|
|
125
|
+
## Requirements
|
|
126
|
+
|
|
127
|
+
- **Node.js 18+**
|
|
128
|
+
- **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** installed and authenticated
|
|
129
|
+
|
|
130
|
+
### Optional: Web Terminal
|
|
131
|
+
|
|
132
|
+
The web terminal feature requires a native module (`node-pty`). mstro works without it - you just won't have the terminal tab in the browser.
|
|
133
|
+
|
|
134
|
+
On first run, mstro will automatically attempt to compile `node-pty`. If your system has build tools installed, it just works. If not, mstro will let you know what to install:
|
|
135
|
+
|
|
136
|
+
- **macOS**: `xcode-select --install`
|
|
137
|
+
- **Linux (Debian/Ubuntu)**: `sudo apt install build-essential python3`
|
|
138
|
+
- **Linux (Fedora/RHEL)**: `sudo dnf install gcc-c++ make python3`
|
|
139
|
+
- **Windows**: `npm install -g windows-build-tools`
|
|
140
|
+
|
|
141
|
+
After installing build tools, run:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
mstro setup-terminal
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Optional: Persistent Terminals
|
|
148
|
+
|
|
149
|
+
Install [tmux](https://github.com/tmux/tmux) for terminal sessions that survive restarts:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# macOS
|
|
153
|
+
brew install tmux
|
|
154
|
+
|
|
155
|
+
# Debian/Ubuntu
|
|
156
|
+
sudo apt install tmux
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Links
|
|
160
|
+
|
|
161
|
+
- **Web App**: [mstro.app](https://mstro.app)
|
|
162
|
+
- **GitHub**: [github.com/mstro-app/mstro](https://github.com/mstro-app/mstro)
|
|
163
|
+
|
|
164
|
+
## Telemetry
|
|
165
|
+
|
|
166
|
+
Mstro collects anonymous error reports and usage data to improve the software. No personal data or code is collected.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
mstro telemetry off # Disable telemetry
|
|
170
|
+
mstro telemetry on # Enable telemetry
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
See [PRIVACY.md](./PRIVACY.md) for details.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* mstro telemetry command
|
|
6
|
+
*
|
|
7
|
+
* Enable or disable anonymous telemetry (error reporting and usage analytics).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* mstro telemetry Show current status
|
|
11
|
+
* mstro telemetry on Enable telemetry
|
|
12
|
+
* mstro telemetry off Disable telemetry
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
// Colors
|
|
20
|
+
const colors = {
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
bold: '\x1b[1m',
|
|
23
|
+
green: '\x1b[32m',
|
|
24
|
+
yellow: '\x1b[33m',
|
|
25
|
+
red: '\x1b[31m',
|
|
26
|
+
dim: '\x1b[2m',
|
|
27
|
+
cyan: '\x1b[36m',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function log(msg, color = '') {
|
|
31
|
+
console.log(`${color}${msg}${colors.reset}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MSTRO_DIR = join(homedir(), '.mstro');
|
|
35
|
+
const CONFIG_FILE = join(MSTRO_DIR, 'config.json');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read current config
|
|
39
|
+
*/
|
|
40
|
+
function readConfig() {
|
|
41
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Write config
|
|
53
|
+
*/
|
|
54
|
+
function writeConfig(config) {
|
|
55
|
+
if (!existsSync(MSTRO_DIR)) {
|
|
56
|
+
mkdirSync(MSTRO_DIR, { recursive: true, mode: 0o700 });
|
|
57
|
+
}
|
|
58
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse on/off value
|
|
63
|
+
*/
|
|
64
|
+
function parseOnOff(value) {
|
|
65
|
+
const lower = value.toLowerCase();
|
|
66
|
+
if (lower === 'true' || lower === '1' || lower === 'on' || lower === 'yes' || lower === 'enable') {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (lower === 'false' || lower === '0' || lower === 'off' || lower === 'no' || lower === 'disable') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function showStatus() {
|
|
76
|
+
const config = readConfig();
|
|
77
|
+
const envDisabled = process.env.MSTRO_TELEMETRY === '0' || process.env.MSTRO_TELEMETRY === 'false';
|
|
78
|
+
const configEnabled = config.telemetry !== false;
|
|
79
|
+
|
|
80
|
+
log('\n Telemetry Status\n', colors.bold + colors.cyan);
|
|
81
|
+
|
|
82
|
+
if (envDisabled) {
|
|
83
|
+
log(' Status: disabled (via MSTRO_TELEMETRY env var)', colors.yellow);
|
|
84
|
+
} else if (!configEnabled) {
|
|
85
|
+
log(' Status: disabled', colors.yellow);
|
|
86
|
+
} else {
|
|
87
|
+
log(' Status: enabled', colors.green);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
log('');
|
|
91
|
+
log(' Mstro collects anonymous error reports and usage data', colors.dim);
|
|
92
|
+
log(' to improve the software. No personal data or code is collected.', colors.dim);
|
|
93
|
+
log('');
|
|
94
|
+
log(' Usage:', colors.bold);
|
|
95
|
+
log(' mstro telemetry on Enable telemetry', colors.dim);
|
|
96
|
+
log(' mstro telemetry off Disable telemetry', colors.dim);
|
|
97
|
+
log('');
|
|
98
|
+
log(' Privacy policy: https://github.com/mstro-app/mstro/blob/main/cli/PRIVACY.md', colors.dim);
|
|
99
|
+
log('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setTelemetry(enabled) {
|
|
103
|
+
const config = readConfig();
|
|
104
|
+
config.telemetry = enabled;
|
|
105
|
+
writeConfig(config);
|
|
106
|
+
|
|
107
|
+
if (enabled) {
|
|
108
|
+
log('\n Telemetry enabled', colors.green);
|
|
109
|
+
log(' Thank you for helping improve mstro!\n', colors.dim);
|
|
110
|
+
} else {
|
|
111
|
+
log('\n Telemetry disabled', colors.yellow);
|
|
112
|
+
log(' No data will be sent.\n', colors.dim);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Main telemetry command
|
|
118
|
+
*/
|
|
119
|
+
export async function telemetry(args = []) {
|
|
120
|
+
const action = args[0];
|
|
121
|
+
|
|
122
|
+
if (!action) {
|
|
123
|
+
showStatus();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (action === '--help' || action === '-h') {
|
|
128
|
+
showStatus();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const value = parseOnOff(action);
|
|
133
|
+
if (value === null) {
|
|
134
|
+
log(`\n Unknown option: ${action}`, colors.red);
|
|
135
|
+
log(' Usage: mstro telemetry [on|off]\n', colors.dim);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
setTelemetry(value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Keep 'config' as alias for backwards compatibility
|
|
143
|
+
export { telemetry as config };
|
|
144
|
+
|
|
145
|
+
export default telemetry;
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mstro login command
|
|
3
|
+
*
|
|
4
|
+
* Authenticates this device with the user's mstro.app account using device code flow.
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. Request device code from platform
|
|
8
|
+
* 2. Open browser to authorization URL
|
|
9
|
+
* 3. Poll platform until user approves
|
|
10
|
+
* 4. Save credentials locally
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { exec } from 'node:child_process';
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { arch, homedir, hostname, type } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
// Colors
|
|
19
|
+
const colors = {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
red: '\x1b[31m',
|
|
26
|
+
dim: '\x1b[2m',
|
|
27
|
+
cyan: '\x1b[36m',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function log(msg, color = '') {
|
|
31
|
+
console.log(`${color}${msg}${colors.reset}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MSTRO_DIR = join(homedir(), '.mstro');
|
|
35
|
+
const CREDENTIALS_FILE = join(MSTRO_DIR, 'credentials.json');
|
|
36
|
+
const CLIENT_ID_FILE = join(MSTRO_DIR, 'client-id');
|
|
37
|
+
const PROD_PLATFORM_URL = 'https://api.mstro.app';
|
|
38
|
+
const DEV_PLATFORM_URL = 'http://localhost:4102';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get or create client ID
|
|
42
|
+
*/
|
|
43
|
+
function getClientId() {
|
|
44
|
+
if (!existsSync(MSTRO_DIR)) {
|
|
45
|
+
mkdirSync(MSTRO_DIR, { recursive: true, mode: 0o700 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (existsSync(CLIENT_ID_FILE)) {
|
|
49
|
+
try {
|
|
50
|
+
const id = readFileSync(CLIENT_ID_FILE, 'utf-8').trim();
|
|
51
|
+
if (id && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)) {
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Generate new
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const newId = crypto.randomUUID();
|
|
60
|
+
writeFileSync(CLIENT_ID_FILE, newId, 'utf-8');
|
|
61
|
+
return newId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if already logged in
|
|
66
|
+
*/
|
|
67
|
+
function isLoggedIn() {
|
|
68
|
+
if (!existsSync(CREDENTIALS_FILE)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const creds = JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'));
|
|
74
|
+
return !!(creds.token && creds.userId && creds.email);
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get stored credentials
|
|
82
|
+
*/
|
|
83
|
+
function getCredentials() {
|
|
84
|
+
if (!existsSync(CREDENTIALS_FILE)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'));
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Save credentials
|
|
96
|
+
*/
|
|
97
|
+
function saveCredentials(creds) {
|
|
98
|
+
if (!existsSync(MSTRO_DIR)) {
|
|
99
|
+
mkdirSync(MSTRO_DIR, { recursive: true, mode: 0o700 });
|
|
100
|
+
}
|
|
101
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Open URL in default browser
|
|
106
|
+
*/
|
|
107
|
+
function openBrowser(url) {
|
|
108
|
+
// Validate URL to prevent command injection via malicious server responses
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = new URL(url);
|
|
112
|
+
} catch {
|
|
113
|
+
log(`\n Invalid URL received. Please open this URL manually:`, colors.yellow);
|
|
114
|
+
log(` ${url}\n`, colors.cyan);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
118
|
+
log(`\n Unexpected URL protocol. Please open this URL manually:`, colors.yellow);
|
|
119
|
+
log(` ${url}\n`, colors.cyan);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const platform = process.platform;
|
|
124
|
+
let cmd;
|
|
125
|
+
|
|
126
|
+
if (platform === 'darwin') {
|
|
127
|
+
cmd = `open "${parsed.href}"`;
|
|
128
|
+
} else if (platform === 'win32') {
|
|
129
|
+
cmd = `start "" "${parsed.href}"`;
|
|
130
|
+
} else {
|
|
131
|
+
cmd = `xdg-open "${parsed.href}"`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
exec(cmd, (err) => {
|
|
135
|
+
if (err) {
|
|
136
|
+
log(`\n Could not open browser automatically.`, colors.yellow);
|
|
137
|
+
log(` Please open this URL manually:`, colors.dim);
|
|
138
|
+
log(` ${url}\n`, colors.cyan);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Request device code from platform
|
|
145
|
+
*/
|
|
146
|
+
async function requestDeviceCode(clientId, platformUrl) {
|
|
147
|
+
const machineHostname = hostname();
|
|
148
|
+
const osType = type().toLowerCase();
|
|
149
|
+
const cpuArch = arch();
|
|
150
|
+
const nodeVersion = process.version;
|
|
151
|
+
|
|
152
|
+
const response = await fetch(`${platformUrl}/api/auth/device/request`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
clientId,
|
|
157
|
+
machineHostname,
|
|
158
|
+
osType,
|
|
159
|
+
cpuArch,
|
|
160
|
+
nodeVersion,
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const text = await response.text();
|
|
165
|
+
let data;
|
|
166
|
+
try {
|
|
167
|
+
data = JSON.parse(text);
|
|
168
|
+
} catch (_e) {
|
|
169
|
+
throw new Error(`Server returned invalid JSON (status ${response.status}): ${text.slice(0, 200)}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw new Error(data.error || data.message || 'Failed to request device code');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return data;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Poll for authorization result
|
|
181
|
+
*/
|
|
182
|
+
async function pollForAuth(deviceCode, interval, platformUrl, maxAttempts = 180) {
|
|
183
|
+
let attempts = 0;
|
|
184
|
+
|
|
185
|
+
while (attempts < maxAttempts) {
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
187
|
+
attempts++;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const response = await fetch(`${platformUrl}/api/auth/device/poll`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({ deviceCode }),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const text = await response.text();
|
|
197
|
+
let data;
|
|
198
|
+
try {
|
|
199
|
+
data = JSON.parse(text);
|
|
200
|
+
} catch (_e) {
|
|
201
|
+
throw new Error(`Server returned invalid JSON (status ${response.status}): ${text.slice(0, 200)}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (response.ok) {
|
|
205
|
+
// Success!
|
|
206
|
+
return data;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Handle specific error codes
|
|
210
|
+
switch (data.error) {
|
|
211
|
+
case 'authorization_pending':
|
|
212
|
+
// Still waiting, continue polling
|
|
213
|
+
process.stdout.write('.');
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case 'expired_token':
|
|
217
|
+
throw new Error('Authorization request expired. Please try again.');
|
|
218
|
+
|
|
219
|
+
case 'access_denied':
|
|
220
|
+
throw new Error('Authorization denied by user.');
|
|
221
|
+
|
|
222
|
+
default:
|
|
223
|
+
throw new Error(data.error || 'Unknown error during authorization');
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
if (err.message.includes('fetch')) {
|
|
227
|
+
// Network error, retry
|
|
228
|
+
process.stdout.write('x');
|
|
229
|
+
} else {
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
throw new Error('Authorization timed out. Please try again.');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Main login command
|
|
240
|
+
*/
|
|
241
|
+
export async function login(args = []) {
|
|
242
|
+
const forceReauth = args.includes('--force') || args.includes('-f');
|
|
243
|
+
const devMode = args.includes('--dev');
|
|
244
|
+
const platformUrl = devMode ? DEV_PLATFORM_URL : PROD_PLATFORM_URL;
|
|
245
|
+
|
|
246
|
+
log('\n Mstro Login\n', colors.bold + colors.cyan);
|
|
247
|
+
|
|
248
|
+
if (devMode) {
|
|
249
|
+
log(` [DEV MODE] Using ${platformUrl}\n`, colors.yellow);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check if already logged in
|
|
253
|
+
if (isLoggedIn() && !forceReauth) {
|
|
254
|
+
const creds = getCredentials();
|
|
255
|
+
log(` Already logged in as ${creds.email}`, colors.green);
|
|
256
|
+
log(` Use "mstro logout" to sign out, or "mstro login --force" to re-authenticate.\n`, colors.dim);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const clientId = getClientId();
|
|
261
|
+
|
|
262
|
+
log(' Requesting authorization...', colors.dim);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Step 1: Request device code
|
|
266
|
+
const { deviceCode, userCode, verificationUrlComplete, interval } = await requestDeviceCode(clientId, platformUrl);
|
|
267
|
+
|
|
268
|
+
// Step 2: Show code and open browser
|
|
269
|
+
log('');
|
|
270
|
+
log(` Your authorization code: ${userCode}`, colors.bold);
|
|
271
|
+
log('');
|
|
272
|
+
log(' Opening browser to complete login...', colors.dim);
|
|
273
|
+
log(` If browser doesn't open, visit: ${verificationUrlComplete}`, colors.dim);
|
|
274
|
+
log('');
|
|
275
|
+
|
|
276
|
+
openBrowser(verificationUrlComplete);
|
|
277
|
+
|
|
278
|
+
// Step 3: Poll for result
|
|
279
|
+
log(' Waiting for authorization', colors.dim);
|
|
280
|
+
process.stdout.write(' ');
|
|
281
|
+
|
|
282
|
+
const result = await pollForAuth(deviceCode, interval, platformUrl);
|
|
283
|
+
|
|
284
|
+
// Step 4: Save credentials
|
|
285
|
+
const credentials = {
|
|
286
|
+
token: result.accessToken,
|
|
287
|
+
userId: result.user.id,
|
|
288
|
+
email: result.user.email,
|
|
289
|
+
name: result.user.name,
|
|
290
|
+
clientId,
|
|
291
|
+
createdAt: new Date().toISOString(),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
saveCredentials(credentials);
|
|
295
|
+
|
|
296
|
+
log('');
|
|
297
|
+
log('');
|
|
298
|
+
log(` Logged in as ${result.user.email}`, colors.bold + colors.green);
|
|
299
|
+
log('');
|
|
300
|
+
log(' This device is now connected to your mstro.app account.', colors.dim);
|
|
301
|
+
log(' Any "mstro" commands will sync with your web dashboard.', colors.dim);
|
|
302
|
+
log('');
|
|
303
|
+
log(' Run "mstro" to start an orchestra.', colors.cyan);
|
|
304
|
+
log('');
|
|
305
|
+
} catch (err) {
|
|
306
|
+
log('');
|
|
307
|
+
log(` Login failed: ${err.message}`, colors.red);
|
|
308
|
+
log('');
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export default login;
|