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
@@ -0,0 +1,435 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Platform Connection Service
6
+ *
7
+ * Handles WebSocket connection to the Mstro platform.
8
+ * Requires token-based authentication from `mstro login`.
9
+ *
10
+ * Flow:
11
+ * 1. Client reads token from ~/.mstro/credentials.json
12
+ * 2. Client connects to platform WebSocket with auth token
13
+ * 3. Platform validates token and auto-pairs to user's account
14
+ * 4. Client becomes an "orchestra" visible in user's web dashboard
15
+ */
16
+
17
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
18
+ import { arch, homedir, hostname, type } from 'node:os'
19
+ import { basename, join } from 'node:path'
20
+ import { getClientId } from './client-id.js'
21
+ import { captureException } from './sentry.js'
22
+ import { isTmuxAvailable } from './terminal/tmux-manager.js'
23
+
24
+ const MSTRO_DIR = join(homedir(), '.mstro')
25
+ const CREDENTIALS_FILE = join(MSTRO_DIR, 'credentials.json')
26
+
27
+ // Refresh token every 30 days
28
+ const TOKEN_REFRESH_INTERVAL_MS = 30 * 24 * 60 * 60 * 1000
29
+
30
+ interface StoredCredentials {
31
+ token: string
32
+ userId: string
33
+ email: string
34
+ name?: string
35
+ clientId: string
36
+ lastRefreshedAt?: string
37
+ }
38
+
39
+ /**
40
+ * Get stored credentials from ~/.mstro/credentials.json
41
+ */
42
+ function getCredentials(): StoredCredentials | null {
43
+ if (!existsSync(CREDENTIALS_FILE)) {
44
+ return null
45
+ }
46
+ try {
47
+ const content = readFileSync(CREDENTIALS_FILE, 'utf-8')
48
+ const creds = JSON.parse(content)
49
+ if (creds.token && creds.userId && creds.email) {
50
+ return creds
51
+ }
52
+ return null
53
+ } catch {
54
+ return null
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Update stored credentials (for token refresh)
60
+ */
61
+ function updateCredentials(updates: Partial<StoredCredentials>): void {
62
+ const creds = getCredentials()
63
+ if (!creds) return
64
+
65
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify({ ...creds, ...updates }, null, 2), {
66
+ mode: 0o600
67
+ })
68
+ }
69
+
70
+ /**
71
+ * Check if token should be refreshed
72
+ */
73
+ function shouldRefreshToken(creds: StoredCredentials): boolean {
74
+ if (!creds.lastRefreshedAt) {
75
+ return true // Never refreshed
76
+ }
77
+
78
+ const lastRefreshed = new Date(creds.lastRefreshedAt).getTime()
79
+ const now = Date.now()
80
+ return now - lastRefreshed > TOKEN_REFRESH_INTERVAL_MS
81
+ }
82
+
83
+ /**
84
+ * Get machine identification string
85
+ * Format: "hostname @ node-vX.X.X platform (arch)"
86
+ * Example: "Jessica @ node-v22.21.1 linux (arm64)"
87
+ */
88
+ export function getMachineIdentifier(): string {
89
+ const machineHostname = hostname()
90
+ const nodeVersion = process.version
91
+ const osType = type().toLowerCase()
92
+ const cpuArch = arch()
93
+ return `${machineHostname} @ node-${nodeVersion} ${osType} (${cpuArch})`
94
+ }
95
+
96
+ // Get WebSocket class - use global if available (Bun, Node 21+), otherwise import from undici (Node 18-20)
97
+ let WebSocketImpl: typeof WebSocket
98
+ if (typeof WebSocket !== 'undefined') {
99
+ WebSocketImpl = WebSocket
100
+ } else {
101
+ // Node 18-20: use undici's WebSocket (bundled with Node.js but not typed)
102
+ // @ts-expect-error undici is bundled with Node.js but lacks type declarations
103
+ const { WebSocket: UndiciWS } = await import('undici')
104
+ WebSocketImpl = UndiciWS as unknown as typeof WebSocket
105
+ }
106
+
107
+ const DEFAULT_PLATFORM_URL = process.env.PLATFORM_URL || 'https://api.mstro.app'
108
+
109
+ interface ConnectionCallbacks {
110
+ onConnected?: (connectionId: string) => void
111
+ onDisconnected?: () => void
112
+ onError?: (error: string) => void
113
+ onWebConnected?: () => void
114
+ onWebDisconnected?: () => void
115
+ onRelayedMessage?: (message: any) => void
116
+ }
117
+
118
+ /**
119
+ * Platform WebSocket connection with token-based authentication
120
+ */
121
+ export class PlatformConnection {
122
+ private ws: WebSocket | null = null
123
+ private reconnectTimeout: ReturnType<typeof setTimeout> | null = null
124
+ private reconnectAttempts = 0
125
+ private maxReconnectAttempts = 10
126
+ private isIntentionallyClosed = false
127
+ private workingDirectory: string
128
+ private platformUrl: string
129
+ private callbacks: ConnectionCallbacks
130
+ private connectionId: string | null = null
131
+ private isConnected = false
132
+ private tokenRefreshInterval: ReturnType<typeof setInterval> | null = null
133
+ private heartbeatInterval: ReturnType<typeof setInterval> | null = null
134
+
135
+ constructor(
136
+ workingDirectory: string,
137
+ callbacks: ConnectionCallbacks = {},
138
+ platformUrl?: string
139
+ ) {
140
+ this.workingDirectory = workingDirectory
141
+ this.platformUrl = platformUrl || DEFAULT_PLATFORM_URL
142
+ this.callbacks = callbacks
143
+ }
144
+
145
+ /**
146
+ * Refresh the device token if needed
147
+ */
148
+ private async maybeRefreshToken(): Promise<void> {
149
+ const creds = getCredentials()
150
+ if (!creds || !shouldRefreshToken(creds)) {
151
+ return
152
+ }
153
+
154
+ try {
155
+ const response = await fetch(`${this.platformUrl}/api/auth/device/refresh`, {
156
+ method: 'POST',
157
+ headers: {
158
+ 'Authorization': `Bearer ${creds.token}`,
159
+ 'Content-Type': 'application/json'
160
+ }
161
+ })
162
+
163
+ if (response.ok) {
164
+ const data = await response.json() as { accessToken: string }
165
+ updateCredentials({
166
+ token: data.accessToken,
167
+ lastRefreshedAt: new Date().toISOString()
168
+ })
169
+ } else {
170
+ console.warn('[Platform] Token refresh failed, will retry later')
171
+ }
172
+ } catch (err) {
173
+ console.warn('[Platform] Token refresh error:', err)
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Start periodic token refresh check
179
+ */
180
+ private startTokenRefreshCheck(): void {
181
+ // Check every 24 hours
182
+ this.tokenRefreshInterval = setInterval(() => {
183
+ this.maybeRefreshToken()
184
+ }, 24 * 60 * 60 * 1000)
185
+ }
186
+
187
+ /**
188
+ * Start heartbeat to keep connection alive and refresh server-side TTL
189
+ */
190
+ private startHeartbeat(): void {
191
+ // Send ping every 2 minutes (server TTL is 5 minutes)
192
+ this.heartbeatInterval = setInterval(() => {
193
+ if (this.ws && this.isConnected) {
194
+ try {
195
+ this.ws.send(JSON.stringify({ type: 'ping' }))
196
+ } catch {
197
+ // Ignore send errors - will reconnect if disconnected
198
+ }
199
+ }
200
+ }, 2 * 60 * 1000)
201
+ }
202
+
203
+ /**
204
+ * Stop heartbeat
205
+ */
206
+ private stopHeartbeat(): void {
207
+ if (this.heartbeatInterval) {
208
+ clearInterval(this.heartbeatInterval)
209
+ this.heartbeatInterval = null
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Stop periodic token refresh check
215
+ */
216
+ private stopTokenRefreshCheck(): void {
217
+ if (this.tokenRefreshInterval) {
218
+ clearInterval(this.tokenRefreshInterval)
219
+ this.tokenRefreshInterval = null
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Connect to platform WebSocket
225
+ */
226
+ connect(): void {
227
+ this.isIntentionallyClosed = false
228
+ const name = basename(this.workingDirectory)
229
+ const machineHostname = hostname()
230
+ const clientId = getClientId()
231
+ const machineId = getMachineIdentifier()
232
+ const nodeVersion = process.version
233
+ const osType = type().toLowerCase()
234
+ const cpuArch = arch()
235
+
236
+ // Get auth token from credentials
237
+ const credentials = getCredentials()
238
+ const authToken = credentials?.token
239
+
240
+ if (!authToken) {
241
+ console.error('\n❌ Not logged in. Run `mstro login` first.\n')
242
+ this.callbacks.onError?.('Not logged in - run `mstro login` first')
243
+ return
244
+ }
245
+
246
+ // Check for tmux availability (for persistent terminals)
247
+ const hasTmux = isTmuxAvailable()
248
+
249
+ // Build URL params WITHOUT the auth token — token is sent post-connection
250
+ // to avoid leaking it in proxy logs, browser history, and server access logs
251
+ const params = new URLSearchParams({
252
+ name,
253
+ workingDirectory: this.workingDirectory,
254
+ machineHostname,
255
+ clientId,
256
+ machineId,
257
+ nodeVersion,
258
+ osType,
259
+ cpuArch,
260
+ capabilities: JSON.stringify({ tmux: hasTmux })
261
+ })
262
+
263
+ const wsUrl = `${this.platformUrl.replace(/^http/, 'ws')}/ws/client?${params}`
264
+
265
+ try {
266
+ this.ws = new WebSocketImpl(wsUrl)
267
+ } catch (err) {
268
+ console.error('Failed to create WebSocket connection:', err)
269
+ captureException(err instanceof Error ? err : new Error(String(err)), { context: 'platform.connect' })
270
+ this.callbacks.onError?.('Failed to connect to platform')
271
+ this.scheduleReconnect()
272
+ return
273
+ }
274
+
275
+ // Connection timeout - if not connected within 10 seconds, show helpful error
276
+ const connectionTimeout = setTimeout(() => {
277
+ const state = this.ws?.readyState
278
+ if (this.ws && (state === 0 || state === undefined)) { // CONNECTING or unknown
279
+ console.error('\n❌ Connection timeout. The platform may have rejected your credentials.')
280
+ console.error(' Run `mstro login --force` to re-authenticate.\n')
281
+ this.ws.close()
282
+ this.callbacks.onError?.('Connection timeout - run `mstro login --force`')
283
+ }
284
+ }, 10000)
285
+
286
+ this.ws.onopen = () => {
287
+ clearTimeout(connectionTimeout)
288
+ console.log(`🌐 Connected to platform`)
289
+
290
+ // Send auth token as first message instead of URL param
291
+ this.ws!.send(JSON.stringify({ type: 'auth', token: authToken }))
292
+
293
+ // Check if token needs refresh on connect
294
+ this.maybeRefreshToken()
295
+ // Start periodic refresh checks
296
+ this.startTokenRefreshCheck()
297
+ this.reconnectAttempts = 0
298
+ }
299
+
300
+ this.ws.onmessage = (event) => {
301
+ try {
302
+ const message = JSON.parse(event.data.toString())
303
+ this.handleMessage(message)
304
+ } catch (err) {
305
+ console.error('Failed to parse platform message:', err)
306
+ }
307
+ }
308
+
309
+ // Track if we ever successfully connected (received 'paired' message)
310
+ let everConnected = false
311
+ const originalOnConnected = this.callbacks.onConnected
312
+ this.callbacks.onConnected = (connectionId) => {
313
+ everConnected = true
314
+ originalOnConnected?.(connectionId)
315
+ }
316
+
317
+ this.ws.onclose = (event) => {
318
+ // Stop heartbeat on any close
319
+ this.stopHeartbeat()
320
+ this.isConnected = false
321
+
322
+ if (!this.isIntentionallyClosed) {
323
+ // Check if we were rejected due to auth (code 4001 or 1006 before ever connecting)
324
+ const isAuthFailure = event.code === 4001 ||
325
+ event.reason?.includes('Unauthorized') ||
326
+ (event.code === 1006 && !everConnected)
327
+
328
+ if (isAuthFailure) {
329
+ console.error('\n❌ Authentication failed. Your device token may be invalid or expired.')
330
+ console.error(' Run `mstro login --force` to re-authenticate.\n')
331
+ this.callbacks.onError?.('Authentication failed - run `mstro login --force`')
332
+ return
333
+ }
334
+
335
+ console.log('Disconnected from platform, reconnecting...')
336
+ this.callbacks.onDisconnected?.()
337
+ this.scheduleReconnect()
338
+ }
339
+ }
340
+
341
+ this.ws.onerror = () => {
342
+ // onclose will be called after this
343
+ }
344
+ }
345
+
346
+ private handleMessage(message: any): void {
347
+ switch (message.type) {
348
+ case 'paired':
349
+ this.isConnected = true
350
+ this.connectionId = message.connectionId
351
+ console.log(`⚡ Connected to mstro.app!`)
352
+ // Start heartbeat to keep server-side TTL refreshed
353
+ this.startHeartbeat()
354
+ this.callbacks.onConnected?.(message.connectionId)
355
+ break
356
+
357
+ case 'web_connected':
358
+ console.log('🔗 Web client connected')
359
+ this.callbacks.onWebConnected?.()
360
+ break
361
+
362
+ case 'web_disconnected':
363
+ console.log('🔗 Web client disconnected')
364
+ this.callbacks.onWebDisconnected?.()
365
+ break
366
+
367
+ case 'pong':
368
+ // Heartbeat response, ignore
369
+ break
370
+
371
+ default:
372
+ // Relay message from web to wsHandler
373
+ // These are messages like 'execute', 'initTab', 'autocomplete', etc.
374
+ this.callbacks.onRelayedMessage?.(message)
375
+ break
376
+ }
377
+ }
378
+
379
+ private scheduleReconnect(): void {
380
+ if (this.reconnectTimeout) return
381
+
382
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
383
+ console.log('Max reconnection attempts reached. Restart "mstro" to try again.')
384
+ return
385
+ }
386
+
387
+ this.reconnectAttempts++
388
+ const delay = Math.min(1000 * 2 ** (this.reconnectAttempts - 1), 30000)
389
+
390
+ this.reconnectTimeout = setTimeout(() => {
391
+ this.reconnectTimeout = null
392
+ this.connect()
393
+ }, delay)
394
+ }
395
+
396
+ /**
397
+ * Send message to platform (will be relayed to web if connected)
398
+ */
399
+ send(message: any): void {
400
+ if (this.ws && this.ws.readyState === WebSocketImpl.OPEN) {
401
+ this.ws.send(JSON.stringify(message))
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Check if connected to platform
407
+ */
408
+ isConnectedToPlatform(): boolean {
409
+ return this.isConnected && this.ws?.readyState === WebSocketImpl.OPEN
410
+ }
411
+
412
+ /**
413
+ * Disconnect from platform
414
+ */
415
+ disconnect(): void {
416
+ this.isIntentionallyClosed = true
417
+
418
+ // Stop heartbeat and token refresh checks
419
+ this.stopHeartbeat()
420
+ this.stopTokenRefreshCheck()
421
+
422
+ if (this.reconnectTimeout) {
423
+ clearTimeout(this.reconnectTimeout)
424
+ this.reconnectTimeout = null
425
+ }
426
+
427
+ if (this.ws) {
428
+ this.ws.close()
429
+ this.ws = null
430
+ }
431
+
432
+ this.isConnected = false
433
+ this.connectionId = null
434
+ }
435
+ }
@@ -0,0 +1,81 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ import { existsSync, readFileSync } from 'node:fs'
5
+ import { homedir } from 'node:os'
6
+ import { join } from 'node:path'
7
+ import * as Sentry from '@sentry/node'
8
+
9
+ // Hardcoded DSN for production - this is safe to expose (can only send, not read)
10
+ // Override with SENTRY_DSN env var for development/testing
11
+ const SENTRY_DSN = process.env.SENTRY_DSN || 'https://2a8d2493e3ee5a7beec30f4518a5e24c@o4510824844820480.ingest.us.sentry.io/4510824923594752'
12
+
13
+ const CONFIG_FILE = join(homedir(), '.mstro', 'config.json')
14
+
15
+ let initialized = false
16
+
17
+ interface MstroConfig {
18
+ telemetry?: boolean
19
+ }
20
+
21
+ /**
22
+ * Check if telemetry/error reporting is enabled
23
+ * Respects the same config as analytics (unified telemetry setting)
24
+ */
25
+ function isTelemetryEnabled(): boolean {
26
+ // Check environment variable first
27
+ const envValue = process.env.MSTRO_TELEMETRY
28
+ if (envValue === '0' || envValue === 'false') {
29
+ return false
30
+ }
31
+
32
+ // Check config file
33
+ if (existsSync(CONFIG_FILE)) {
34
+ try {
35
+ const config: MstroConfig = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
36
+ if (config.telemetry === false) {
37
+ return false
38
+ }
39
+ } catch {
40
+ // Ignore parse errors
41
+ }
42
+ }
43
+
44
+ return true
45
+ }
46
+
47
+ export function initSentry(): void {
48
+ if (initialized) return
49
+ if (!isTelemetryEnabled()) return
50
+
51
+ initialized = true
52
+
53
+ Sentry.init({
54
+ dsn: SENTRY_DSN,
55
+ environment: process.env.NODE_ENV || 'development',
56
+ release: `mstro-cli@${process.env.npm_package_version || '0.0.0'}`,
57
+ tracesSampleRate: 0.1,
58
+ beforeSend(event) {
59
+ // Strip PII from error events
60
+ if (event.user) {
61
+ delete event.user.ip_address
62
+ }
63
+ return event
64
+ },
65
+ })
66
+ }
67
+
68
+ export function captureException(error: unknown, context?: Record<string, any>): void {
69
+ if (!initialized) return
70
+ Sentry.captureException(error, context ? { extra: context } : undefined)
71
+ }
72
+
73
+ export function captureMessage(message: string, level: 'info' | 'warning' | 'error' = 'info'): void {
74
+ if (!initialized) return
75
+ Sentry.captureMessage(message, level)
76
+ }
77
+
78
+ export async function flushSentry(timeout = 2000): Promise<void> {
79
+ if (!initialized) return
80
+ await Sentry.flush(timeout)
81
+ }