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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/bin/commands/config.js +145 -0
  4. package/bin/commands/login.js +313 -0
  5. package/bin/commands/logout.js +75 -0
  6. package/bin/commands/status.js +197 -0
  7. package/bin/commands/whoami.js +161 -0
  8. package/bin/configure-claude.js +298 -0
  9. package/bin/mstro.js +581 -0
  10. package/bin/postinstall.js +45 -0
  11. package/bin/release.sh +110 -0
  12. package/dist/server/cli/headless/claude-invoker.d.ts +17 -0
  13. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -0
  14. package/dist/server/cli/headless/claude-invoker.js +311 -0
  15. package/dist/server/cli/headless/claude-invoker.js.map +1 -0
  16. package/dist/server/cli/headless/index.d.ts +13 -0
  17. package/dist/server/cli/headless/index.d.ts.map +1 -0
  18. package/dist/server/cli/headless/index.js +10 -0
  19. package/dist/server/cli/headless/index.js.map +1 -0
  20. package/dist/server/cli/headless/mcp-config.d.ts +11 -0
  21. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -0
  22. package/dist/server/cli/headless/mcp-config.js +76 -0
  23. package/dist/server/cli/headless/mcp-config.js.map +1 -0
  24. package/dist/server/cli/headless/output-utils.d.ts +33 -0
  25. package/dist/server/cli/headless/output-utils.d.ts.map +1 -0
  26. package/dist/server/cli/headless/output-utils.js +101 -0
  27. package/dist/server/cli/headless/output-utils.js.map +1 -0
  28. package/dist/server/cli/headless/prompt-utils.d.ts +21 -0
  29. package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -0
  30. package/dist/server/cli/headless/prompt-utils.js +84 -0
  31. package/dist/server/cli/headless/prompt-utils.js.map +1 -0
  32. package/dist/server/cli/headless/runner.d.ts +24 -0
  33. package/dist/server/cli/headless/runner.d.ts.map +1 -0
  34. package/dist/server/cli/headless/runner.js +99 -0
  35. package/dist/server/cli/headless/runner.js.map +1 -0
  36. package/dist/server/cli/headless/types.d.ts +106 -0
  37. package/dist/server/cli/headless/types.d.ts.map +1 -0
  38. package/dist/server/cli/headless/types.js +4 -0
  39. package/dist/server/cli/headless/types.js.map +1 -0
  40. package/dist/server/cli/improvisation-session-manager.d.ts +155 -0
  41. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -0
  42. package/dist/server/cli/improvisation-session-manager.js +415 -0
  43. package/dist/server/cli/improvisation-session-manager.js.map +1 -0
  44. package/dist/server/index.d.ts +2 -0
  45. package/dist/server/index.d.ts.map +1 -0
  46. package/dist/server/index.js +386 -0
  47. package/dist/server/index.js.map +1 -0
  48. package/dist/server/mcp/bouncer-cli.d.ts +3 -0
  49. package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
  50. package/dist/server/mcp/bouncer-cli.js +99 -0
  51. package/dist/server/mcp/bouncer-cli.js.map +1 -0
  52. package/dist/server/mcp/bouncer-integration.d.ts +36 -0
  53. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -0
  54. package/dist/server/mcp/bouncer-integration.js +301 -0
  55. package/dist/server/mcp/bouncer-integration.js.map +1 -0
  56. package/dist/server/mcp/security-audit.d.ts +52 -0
  57. package/dist/server/mcp/security-audit.d.ts.map +1 -0
  58. package/dist/server/mcp/security-audit.js +118 -0
  59. package/dist/server/mcp/security-audit.js.map +1 -0
  60. package/dist/server/mcp/security-patterns.d.ts +73 -0
  61. package/dist/server/mcp/security-patterns.d.ts.map +1 -0
  62. package/dist/server/mcp/security-patterns.js +247 -0
  63. package/dist/server/mcp/security-patterns.js.map +1 -0
  64. package/dist/server/mcp/server.d.ts +3 -0
  65. package/dist/server/mcp/server.d.ts.map +1 -0
  66. package/dist/server/mcp/server.js +146 -0
  67. package/dist/server/mcp/server.js.map +1 -0
  68. package/dist/server/routes/files.d.ts +9 -0
  69. package/dist/server/routes/files.d.ts.map +1 -0
  70. package/dist/server/routes/files.js +24 -0
  71. package/dist/server/routes/files.js.map +1 -0
  72. package/dist/server/routes/improvise.d.ts +3 -0
  73. package/dist/server/routes/improvise.d.ts.map +1 -0
  74. package/dist/server/routes/improvise.js +72 -0
  75. package/dist/server/routes/improvise.js.map +1 -0
  76. package/dist/server/routes/index.d.ts +10 -0
  77. package/dist/server/routes/index.d.ts.map +1 -0
  78. package/dist/server/routes/index.js +12 -0
  79. package/dist/server/routes/index.js.map +1 -0
  80. package/dist/server/routes/instances.d.ts +10 -0
  81. package/dist/server/routes/instances.d.ts.map +1 -0
  82. package/dist/server/routes/instances.js +47 -0
  83. package/dist/server/routes/instances.js.map +1 -0
  84. package/dist/server/routes/notifications.d.ts +3 -0
  85. package/dist/server/routes/notifications.d.ts.map +1 -0
  86. package/dist/server/routes/notifications.js +136 -0
  87. package/dist/server/routes/notifications.js.map +1 -0
  88. package/dist/server/services/analytics.d.ts +56 -0
  89. package/dist/server/services/analytics.d.ts.map +1 -0
  90. package/dist/server/services/analytics.js +240 -0
  91. package/dist/server/services/analytics.js.map +1 -0
  92. package/dist/server/services/auth.d.ts +26 -0
  93. package/dist/server/services/auth.d.ts.map +1 -0
  94. package/dist/server/services/auth.js +71 -0
  95. package/dist/server/services/auth.js.map +1 -0
  96. package/dist/server/services/client-id.d.ts +10 -0
  97. package/dist/server/services/client-id.d.ts.map +1 -0
  98. package/dist/server/services/client-id.js +61 -0
  99. package/dist/server/services/client-id.js.map +1 -0
  100. package/dist/server/services/credentials.d.ts +39 -0
  101. package/dist/server/services/credentials.d.ts.map +1 -0
  102. package/dist/server/services/credentials.js +110 -0
  103. package/dist/server/services/credentials.js.map +1 -0
  104. package/dist/server/services/files.d.ts +119 -0
  105. package/dist/server/services/files.d.ts.map +1 -0
  106. package/dist/server/services/files.js +560 -0
  107. package/dist/server/services/files.js.map +1 -0
  108. package/dist/server/services/instances.d.ts +52 -0
  109. package/dist/server/services/instances.d.ts.map +1 -0
  110. package/dist/server/services/instances.js +241 -0
  111. package/dist/server/services/instances.js.map +1 -0
  112. package/dist/server/services/pathUtils.d.ts +47 -0
  113. package/dist/server/services/pathUtils.d.ts.map +1 -0
  114. package/dist/server/services/pathUtils.js +124 -0
  115. package/dist/server/services/pathUtils.js.map +1 -0
  116. package/dist/server/services/platform.d.ts +72 -0
  117. package/dist/server/services/platform.d.ts.map +1 -0
  118. package/dist/server/services/platform.js +368 -0
  119. package/dist/server/services/platform.js.map +1 -0
  120. package/dist/server/services/sentry.d.ts +5 -0
  121. package/dist/server/services/sentry.d.ts.map +1 -0
  122. package/dist/server/services/sentry.js +71 -0
  123. package/dist/server/services/sentry.js.map +1 -0
  124. package/dist/server/services/terminal/pty-manager.d.ts +149 -0
  125. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -0
  126. package/dist/server/services/terminal/pty-manager.js +377 -0
  127. package/dist/server/services/terminal/pty-manager.js.map +1 -0
  128. package/dist/server/services/terminal/tmux-manager.d.ts +82 -0
  129. package/dist/server/services/terminal/tmux-manager.d.ts.map +1 -0
  130. package/dist/server/services/terminal/tmux-manager.js +352 -0
  131. package/dist/server/services/terminal/tmux-manager.js.map +1 -0
  132. package/dist/server/services/websocket/autocomplete.d.ts +50 -0
  133. package/dist/server/services/websocket/autocomplete.d.ts.map +1 -0
  134. package/dist/server/services/websocket/autocomplete.js +361 -0
  135. package/dist/server/services/websocket/autocomplete.js.map +1 -0
  136. package/dist/server/services/websocket/file-utils.d.ts +44 -0
  137. package/dist/server/services/websocket/file-utils.d.ts.map +1 -0
  138. package/dist/server/services/websocket/file-utils.js +272 -0
  139. package/dist/server/services/websocket/file-utils.js.map +1 -0
  140. package/dist/server/services/websocket/handler.d.ts +246 -0
  141. package/dist/server/services/websocket/handler.d.ts.map +1 -0
  142. package/dist/server/services/websocket/handler.js +1771 -0
  143. package/dist/server/services/websocket/handler.js.map +1 -0
  144. package/dist/server/services/websocket/index.d.ts +11 -0
  145. package/dist/server/services/websocket/index.d.ts.map +1 -0
  146. package/dist/server/services/websocket/index.js +14 -0
  147. package/dist/server/services/websocket/index.js.map +1 -0
  148. package/dist/server/services/websocket/types.d.ts +214 -0
  149. package/dist/server/services/websocket/types.d.ts.map +1 -0
  150. package/dist/server/services/websocket/types.js +4 -0
  151. package/dist/server/services/websocket/types.js.map +1 -0
  152. package/dist/server/utils/agent-manager.d.ts +69 -0
  153. package/dist/server/utils/agent-manager.d.ts.map +1 -0
  154. package/dist/server/utils/agent-manager.js +269 -0
  155. package/dist/server/utils/agent-manager.js.map +1 -0
  156. package/dist/server/utils/paths.d.ts +25 -0
  157. package/dist/server/utils/paths.d.ts.map +1 -0
  158. package/dist/server/utils/paths.js +38 -0
  159. package/dist/server/utils/paths.js.map +1 -0
  160. package/dist/server/utils/port-manager.d.ts +10 -0
  161. package/dist/server/utils/port-manager.d.ts.map +1 -0
  162. package/dist/server/utils/port-manager.js +60 -0
  163. package/dist/server/utils/port-manager.js.map +1 -0
  164. package/dist/server/utils/port.d.ts +26 -0
  165. package/dist/server/utils/port.d.ts.map +1 -0
  166. package/dist/server/utils/port.js +83 -0
  167. package/dist/server/utils/port.js.map +1 -0
  168. package/hooks/bouncer.sh +138 -0
  169. package/package.json +74 -0
  170. package/server/README.md +191 -0
  171. package/server/cli/headless/claude-invoker.ts +415 -0
  172. package/server/cli/headless/index.ts +39 -0
  173. package/server/cli/headless/mcp-config.ts +87 -0
  174. package/server/cli/headless/output-utils.ts +109 -0
  175. package/server/cli/headless/prompt-utils.ts +108 -0
  176. package/server/cli/headless/runner.ts +133 -0
  177. package/server/cli/headless/types.ts +118 -0
  178. package/server/cli/improvisation-session-manager.ts +531 -0
  179. package/server/index.ts +456 -0
  180. package/server/mcp/README.md +122 -0
  181. package/server/mcp/bouncer-cli.ts +127 -0
  182. package/server/mcp/bouncer-integration.ts +430 -0
  183. package/server/mcp/security-audit.ts +180 -0
  184. package/server/mcp/security-patterns.ts +290 -0
  185. package/server/mcp/server.ts +174 -0
  186. package/server/routes/files.ts +29 -0
  187. package/server/routes/improvise.ts +82 -0
  188. package/server/routes/index.ts +13 -0
  189. package/server/routes/instances.ts +54 -0
  190. package/server/routes/notifications.ts +158 -0
  191. package/server/services/analytics.ts +277 -0
  192. package/server/services/auth.ts +80 -0
  193. package/server/services/client-id.ts +68 -0
  194. package/server/services/credentials.ts +134 -0
  195. package/server/services/files.ts +710 -0
  196. package/server/services/instances.ts +275 -0
  197. package/server/services/pathUtils.ts +158 -0
  198. package/server/services/platform.test.ts +1314 -0
  199. package/server/services/platform.ts +435 -0
  200. package/server/services/sentry.ts +81 -0
  201. package/server/services/terminal/pty-manager.ts +464 -0
  202. package/server/services/terminal/tmux-manager.ts +426 -0
  203. package/server/services/websocket/autocomplete.ts +438 -0
  204. package/server/services/websocket/file-utils.ts +305 -0
  205. package/server/services/websocket/handler.test.ts +20 -0
  206. package/server/services/websocket/handler.ts +2047 -0
  207. package/server/services/websocket/index.ts +40 -0
  208. package/server/services/websocket/types.ts +339 -0
  209. package/server/tsconfig.json +19 -0
  210. package/server/utils/agent-manager.ts +323 -0
  211. package/server/utils/paths.ts +45 -0
  212. package/server/utils/port-manager.ts +70 -0
  213. 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;