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
|
@@ -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
|
+
}
|