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,158 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Notification Routes
6
+ *
7
+ * Handles notification-related operations including AI-powered summary generation.
8
+ */
9
+
10
+ import { spawn } from 'node:child_process'
11
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
12
+ import { join } from 'node:path'
13
+ import { Hono } from 'hono'
14
+
15
+ export function createNotificationRoutes(workingDir: string) {
16
+ const routes = new Hono()
17
+
18
+ /**
19
+ * Generate a summary for browser notification using Claude Haiku
20
+ * POST /summarize
21
+ * Body: { prompt: string, output: string }
22
+ * Returns: { summary: string }
23
+ */
24
+ routes.post('/summarize', async (c) => {
25
+ try {
26
+ const body = await c.req.json()
27
+ const { prompt, output } = body
28
+
29
+ if (!prompt || !output) {
30
+ return c.json({ error: 'Missing required fields: prompt and output' }, 400)
31
+ }
32
+
33
+ const summary = await generateNotificationSummary(prompt, output, workingDir)
34
+ return c.json({ summary })
35
+ } catch (error) {
36
+ console.error('[Notifications] Error generating summary:', error)
37
+ // Return a fallback summary on error
38
+ return c.json({ summary: 'Task completed' })
39
+ }
40
+ })
41
+
42
+ return routes
43
+ }
44
+
45
+ /**
46
+ * Generate a notification summary using Claude Haiku
47
+ */
48
+ async function generateNotificationSummary(
49
+ userPrompt: string,
50
+ output: string,
51
+ workingDir: string
52
+ ): Promise<string> {
53
+ return new Promise((resolve) => {
54
+ // Create temp directory if it doesn't exist
55
+ const tempDir = join(workingDir, '.mstro', 'tmp')
56
+ if (!existsSync(tempDir)) {
57
+ mkdirSync(tempDir, { recursive: true })
58
+ }
59
+
60
+ // Truncate output if too long (keep first and last parts for context)
61
+ let truncatedOutput = output
62
+ if (output.length > 4000) {
63
+ const firstPart = output.slice(0, 2000)
64
+ const lastPart = output.slice(-1500)
65
+ truncatedOutput = `${firstPart}\n\n... [output truncated] ...\n\n${lastPart}`
66
+ }
67
+
68
+ // Build the prompt for summary generation
69
+ const summaryPrompt = `You are generating a SHORT browser notification summary for a completed task.
70
+ The user ran a task and wants a brief notification to remind them what happened.
71
+
72
+ USER'S ORIGINAL PROMPT:
73
+ "${userPrompt}"
74
+
75
+ TASK OUTPUT (may be truncated):
76
+ ${truncatedOutput}
77
+
78
+ Generate a notification summary following these rules:
79
+ 1. Maximum 100 characters (this is a browser notification)
80
+ 2. Focus on the OUTCOME, not the process
81
+ 3. Be specific about what was accomplished
82
+ 4. Use past tense (e.g., "Fixed bug in auth.ts", "Added 3 new tests")
83
+ 5. If there was an error, mention it briefly
84
+ 6. No emojis, no markdown, just plain text
85
+
86
+ Respond with ONLY the summary text, nothing else.`
87
+
88
+ // Write prompt to temp file
89
+ const promptFile = join(tempDir, `notif-summary-${Date.now()}.txt`)
90
+ writeFileSync(promptFile, summaryPrompt)
91
+
92
+ const systemPrompt = 'You are a notification summary assistant. Respond with only the summary text, no preamble or explanation.'
93
+
94
+ const args = [
95
+ '--print',
96
+ '--model', 'haiku',
97
+ '--system-prompt', systemPrompt,
98
+ promptFile
99
+ ]
100
+
101
+ const claude = spawn('claude', args, {
102
+ cwd: workingDir,
103
+ stdio: ['ignore', 'pipe', 'pipe']
104
+ })
105
+
106
+ let stdout = ''
107
+ let stderr = ''
108
+
109
+ claude.stdout?.on('data', (data) => {
110
+ stdout += data.toString()
111
+ })
112
+
113
+ claude.stderr?.on('data', (data) => {
114
+ stderr += data.toString()
115
+ })
116
+
117
+ claude.on('close', (code) => {
118
+ // Clean up temp file
119
+ try {
120
+ unlinkSync(promptFile)
121
+ } catch {
122
+ // Ignore cleanup errors
123
+ }
124
+
125
+ if (code === 0 && stdout.trim()) {
126
+ // Truncate if somehow still too long
127
+ const summary = stdout.trim().slice(0, 150)
128
+ resolve(summary)
129
+ } else {
130
+ console.error('[Notifications] Claude error:', stderr || 'Unknown error')
131
+ // Fallback to basic summary
132
+ resolve(createFallbackSummary(userPrompt))
133
+ }
134
+ })
135
+
136
+ claude.on('error', (err) => {
137
+ console.error('[Notifications] Failed to spawn Claude:', err)
138
+ resolve(createFallbackSummary(userPrompt))
139
+ })
140
+
141
+ // Timeout after 10 seconds
142
+ setTimeout(() => {
143
+ claude.kill()
144
+ resolve(createFallbackSummary(userPrompt))
145
+ }, 10000)
146
+ })
147
+ }
148
+
149
+ /**
150
+ * Create a fallback summary when AI summarization fails
151
+ */
152
+ function createFallbackSummary(userPrompt: string): string {
153
+ const truncated = userPrompt.slice(0, 60)
154
+ if (userPrompt.length > 60) {
155
+ return `Completed: "${truncated}..."`
156
+ }
157
+ return `Completed: "${truncated}"`
158
+ }
@@ -0,0 +1,277 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * PostHog Analytics Service for mstro CLI
6
+ *
7
+ * Provides analytics tracking for the mstro client.
8
+ * Uses PostHog Node SDK for server-side event tracking.
9
+ *
10
+ * Config is fetched from platform server (not hardcoded) so the
11
+ * PostHog key isn't exposed in the npm package.
12
+ *
13
+ * Telemetry is opt-out by default. Users can disable with:
14
+ * - Command: mstro telemetry off
15
+ * - Environment variable: MSTRO_TELEMETRY=0
16
+ */
17
+
18
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
19
+ import { arch, homedir, platform } from 'node:os'
20
+ import { join } from 'node:path'
21
+ import { PostHog } from 'posthog-node'
22
+ import { getClientId } from './client-id.js'
23
+
24
+ const MSTRO_DIR = join(homedir(), '.mstro')
25
+ const CONFIG_FILE = join(MSTRO_DIR, 'config.json')
26
+ const PLATFORM_URL = process.env.PLATFORM_URL || 'https://api.mstro.app'
27
+
28
+ let client: PostHog | null = null
29
+ let telemetryEnabled: boolean | null = null
30
+ let analyticsConfig: { posthogKey: string; posthogHost: string } | null = null
31
+
32
+ interface MstroConfig {
33
+ telemetry?: boolean
34
+ }
35
+
36
+ /**
37
+ * Check if telemetry is enabled
38
+ */
39
+ function isTelemetryEnabled(): boolean {
40
+ if (telemetryEnabled !== null) {
41
+ return telemetryEnabled
42
+ }
43
+
44
+ // Check environment variable first
45
+ const envValue = process.env.MSTRO_TELEMETRY
46
+ if (envValue === '0' || envValue === 'false') {
47
+ telemetryEnabled = false
48
+ return false
49
+ }
50
+
51
+ // Check config file
52
+ if (existsSync(CONFIG_FILE)) {
53
+ try {
54
+ const config: MstroConfig = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
55
+ if (config.telemetry === false) {
56
+ telemetryEnabled = false
57
+ return false
58
+ }
59
+ } catch {
60
+ // Ignore parse errors
61
+ }
62
+ }
63
+
64
+ telemetryEnabled = true
65
+ return true
66
+ }
67
+
68
+ /**
69
+ * Fetch analytics config from platform server
70
+ * This keeps the PostHog key out of the npm package
71
+ */
72
+ async function fetchAnalyticsConfig(): Promise<{ posthogKey: string; posthogHost: string } | null> {
73
+ try {
74
+ const response = await fetch(`${PLATFORM_URL}/api/config/client`, {
75
+ method: 'GET',
76
+ headers: { 'Content-Type': 'application/json' },
77
+ })
78
+
79
+ if (!response.ok) {
80
+ return null
81
+ }
82
+
83
+ const data = await response.json() as { analytics?: { posthogKey?: string; posthogHost?: string } }
84
+ return {
85
+ posthogKey: data.analytics?.posthogKey || '',
86
+ posthogHost: data.analytics?.posthogHost || 'https://eu.i.posthog.com',
87
+ }
88
+ } catch {
89
+ // Network error, platform unavailable - silently disable analytics
90
+ return null
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Initialize PostHog client
96
+ * Call this once at server startup
97
+ */
98
+ export async function initAnalytics(): Promise<void> {
99
+ if (!isTelemetryEnabled()) {
100
+ return
101
+ }
102
+
103
+ // Fetch config from platform
104
+ analyticsConfig = await fetchAnalyticsConfig()
105
+
106
+ if (!analyticsConfig?.posthogKey) {
107
+ // No key configured on platform, analytics disabled
108
+ return
109
+ }
110
+
111
+ client = new PostHog(analyticsConfig.posthogKey, {
112
+ host: analyticsConfig.posthogHost,
113
+ // Flush events every 10 seconds or 20 events
114
+ flushAt: 20,
115
+ flushInterval: 10000,
116
+ })
117
+ }
118
+
119
+ /**
120
+ * Shutdown PostHog client gracefully
121
+ * Call this before process exit
122
+ */
123
+ export async function shutdownAnalytics(): Promise<void> {
124
+ if (client) {
125
+ await client.shutdown()
126
+ client = null
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Get the distinct ID for this client
132
+ * Uses the persistent client ID from ~/.mstro/client-id
133
+ */
134
+ function getDistinctId(): string {
135
+ return getClientId()
136
+ }
137
+
138
+ /**
139
+ * Get common properties included with all events
140
+ */
141
+ function getCommonProperties(): Record<string, any> {
142
+ return {
143
+ os: platform(),
144
+ arch: arch(),
145
+ node_version: process.version,
146
+ mstro_version: process.env.npm_package_version || 'unknown',
147
+ source: 'client',
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Track a custom event
153
+ */
154
+ export function trackEvent(event: string, properties?: Record<string, any>): void {
155
+ if (!client || !isTelemetryEnabled()) return
156
+
157
+ client.capture({
158
+ distinctId: getDistinctId(),
159
+ event,
160
+ properties: {
161
+ ...getCommonProperties(),
162
+ ...properties,
163
+ },
164
+ })
165
+ }
166
+
167
+ /**
168
+ * Identify a user (call after login)
169
+ */
170
+ export function identifyUser(userId: string, properties?: Record<string, any>): void {
171
+ if (!client || !isTelemetryEnabled()) return
172
+
173
+ // Link the client ID to the user ID
174
+ client.alias({
175
+ distinctId: userId,
176
+ alias: getDistinctId(),
177
+ })
178
+
179
+ client.identify({
180
+ distinctId: userId,
181
+ properties: {
182
+ ...getCommonProperties(),
183
+ ...properties,
184
+ },
185
+ })
186
+ }
187
+
188
+ /**
189
+ * Set telemetry preference in config file
190
+ */
191
+ export function setTelemetryEnabled(enabled: boolean): void {
192
+ let config: MstroConfig = {}
193
+
194
+ if (existsSync(CONFIG_FILE)) {
195
+ try {
196
+ config = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
197
+ } catch {
198
+ // Start fresh if parse fails
199
+ }
200
+ }
201
+
202
+ config.telemetry = enabled
203
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 })
204
+
205
+ // Update cached value
206
+ telemetryEnabled = enabled
207
+ }
208
+
209
+ /**
210
+ * Get current telemetry status
211
+ */
212
+ export function getTelemetryStatus(): { enabled: boolean; reason: string } {
213
+ const envValue = process.env.MSTRO_TELEMETRY
214
+ if (envValue === '0' || envValue === 'false') {
215
+ return { enabled: false, reason: 'Disabled via MSTRO_TELEMETRY environment variable' }
216
+ }
217
+
218
+ if (existsSync(CONFIG_FILE)) {
219
+ try {
220
+ const config: MstroConfig = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
221
+ if (config.telemetry === false) {
222
+ return { enabled: false, reason: 'Disabled via ~/.mstro/config.json' }
223
+ }
224
+ } catch {
225
+ // Ignore
226
+ }
227
+ }
228
+
229
+ if (!analyticsConfig?.posthogKey) {
230
+ return { enabled: false, reason: 'Analytics not configured on platform' }
231
+ }
232
+
233
+ return { enabled: true, reason: 'Enabled (opt-out with MSTRO_TELEMETRY=0)' }
234
+ }
235
+
236
+ // ===========================================
237
+ // Event Constants - Use these for consistency
238
+ // ===========================================
239
+
240
+ export const AnalyticsEvents = {
241
+ // CLI events
242
+ CLI_STARTED: 'cli_started',
243
+ CLI_COMMAND: 'cli_command',
244
+ CLI_LOGIN: 'cli_login',
245
+ CLI_LOGOUT: 'cli_logout',
246
+ CLI_ERROR: 'cli_error',
247
+
248
+ // Server events
249
+ SERVER_STARTED: 'server_started',
250
+ SERVER_STOPPED: 'server_stopped',
251
+
252
+ // Connection events
253
+ PLATFORM_CONNECTED: 'platform_connected',
254
+ PLATFORM_DISCONNECTED: 'platform_disconnected',
255
+ WEB_CLIENT_CONNECTED: 'web_client_connected',
256
+ WEB_CLIENT_DISCONNECTED: 'web_client_disconnected',
257
+
258
+ // Improvise events
259
+ IMPROVISE_SESSION_STARTED: 'improvise_session_started',
260
+ IMPROVISE_PROMPT_RECEIVED: 'improvise_prompt_received',
261
+ IMPROVISE_MOVEMENT_STARTED: 'improvise_movement_started',
262
+ IMPROVISE_MOVEMENT_COMPLETED: 'improvise_movement_completed',
263
+ IMPROVISE_MOVEMENT_ERROR: 'improvise_movement_error',
264
+ IMPROVISE_SESSION_ENDED: 'improvise_session_ended',
265
+ IMPROVISE_ABORTED: 'improvise_aborted',
266
+
267
+ // Terminal events
268
+ TERMINAL_SESSION_CREATED: 'terminal_session_created',
269
+ TERMINAL_SESSION_CLOSED: 'terminal_session_closed',
270
+
271
+ // MCP/Bouncer events
272
+ BOUNCER_TOOL_ALLOWED: 'bouncer_tool_allowed',
273
+ BOUNCER_TOOL_DENIED: 'bouncer_tool_denied',
274
+ BOUNCER_HAIKU_REVIEW: 'bouncer_haiku_review',
275
+ } as const
276
+
277
+ export type AnalyticsEvent = typeof AnalyticsEvents[keyof typeof AnalyticsEvents]
@@ -0,0 +1,80 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Authentication Service
6
+ *
7
+ * Manages local session token for localhost API/WebSocket auth.
8
+ * All mstro instances on a machine share a single token from ~/.mstro/session-token.
9
+ * The token is created once (by `mstro login` or first server start) and reused.
10
+ */
11
+
12
+ import { randomBytes } from 'node:crypto'
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
14
+ import { homedir } from 'node:os'
15
+ import { join } from 'node:path'
16
+
17
+ const SESSION_TOKEN_PATH = join(homedir(), '.mstro', 'session-token')
18
+
19
+ export class AuthService {
20
+ private localToken: string
21
+
22
+ constructor() {
23
+ this.localToken = this.loadOrCreateToken()
24
+ }
25
+
26
+ /**
27
+ * Load existing session token from disk, or create one if missing.
28
+ * This ensures all mstro instances on the same machine share the same token.
29
+ */
30
+ private loadOrCreateToken(): string {
31
+ const existing = AuthService.readLocalToken()
32
+ if (existing) {
33
+ return existing
34
+ }
35
+
36
+ const token = randomBytes(32).toString('hex')
37
+ this.writeToken(token)
38
+ return token
39
+ }
40
+
41
+ /**
42
+ * Write session token to ~/.mstro/session-token
43
+ */
44
+ private writeToken(token: string): void {
45
+ const dir = join(homedir(), '.mstro')
46
+ if (!existsSync(dir)) {
47
+ mkdirSync(dir, { recursive: true, mode: 0o700 })
48
+ }
49
+ writeFileSync(SESSION_TOKEN_PATH, token, { mode: 0o600 })
50
+ }
51
+
52
+ /**
53
+ * Validate the local session token (used for localhost API/WS auth)
54
+ */
55
+ validateLocalToken(token: string): boolean {
56
+ return token === this.localToken
57
+ }
58
+
59
+ /**
60
+ * Get the local session token (for passing to child processes)
61
+ */
62
+ getLocalToken(): string {
63
+ return this.localToken
64
+ }
65
+
66
+ /**
67
+ * Read the local session token from disk (static utility for clients)
68
+ */
69
+ static readLocalToken(): string | null {
70
+ try {
71
+ if (existsSync(SESSION_TOKEN_PATH)) {
72
+ const token = readFileSync(SESSION_TOKEN_PATH, 'utf-8').trim()
73
+ if (token.length > 0) {
74
+ return token
75
+ }
76
+ }
77
+ } catch {}
78
+ return null
79
+ }
80
+ }
@@ -0,0 +1,68 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Client ID Service
6
+ *
7
+ * Generates and persists a unique client identifier that survives server restarts.
8
+ * This ID is used to distinguish between different user home directories on different machines.
9
+ *
10
+ * Storage: ~/.mstro/client-id
11
+ */
12
+
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
14
+ import { homedir } from 'node:os'
15
+ import { join } from 'node:path'
16
+
17
+ const MSTRO_DIR = join(homedir(), '.mstro')
18
+ const CLIENT_ID_FILE = join(MSTRO_DIR, 'client-id')
19
+
20
+ /**
21
+ * Generate a new UUID v4
22
+ */
23
+ function generateUUID(): string {
24
+ return crypto.randomUUID()
25
+ }
26
+
27
+ /**
28
+ * Get the persistent client ID, creating one if it doesn't exist.
29
+ * This ID is unique to this machine's ~/.mstro directory.
30
+ */
31
+ export function getClientId(): string {
32
+ // Ensure ~/.mstro directory exists
33
+ if (!existsSync(MSTRO_DIR)) {
34
+ mkdirSync(MSTRO_DIR, { recursive: true, mode: 0o700 })
35
+ }
36
+
37
+ // Try to read existing client ID
38
+ if (existsSync(CLIENT_ID_FILE)) {
39
+ try {
40
+ const id = readFileSync(CLIENT_ID_FILE, 'utf-8').trim()
41
+ if (id && isValidUUID(id)) {
42
+ return id
43
+ }
44
+ } catch {
45
+ // File exists but couldn't be read, generate new one
46
+ }
47
+ }
48
+
49
+ // Generate and persist a new client ID
50
+ const newId = generateUUID()
51
+ writeFileSync(CLIENT_ID_FILE, newId, 'utf-8')
52
+ return newId
53
+ }
54
+
55
+ /**
56
+ * Validate UUID format
57
+ */
58
+ function isValidUUID(id: string): boolean {
59
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
60
+ return uuidRegex.test(id)
61
+ }
62
+
63
+ /**
64
+ * Get the client ID file path (for debugging/display)
65
+ */
66
+ export function getClientIdPath(): string {
67
+ return CLIENT_ID_FILE
68
+ }
@@ -0,0 +1,134 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Credentials Service
6
+ *
7
+ * Manages persistent authentication credentials stored in ~/.mstro/credentials.json
8
+ *
9
+ * Structure:
10
+ * {
11
+ * "token": "device-token-here",
12
+ * "userId": "user-uuid",
13
+ * "email": "user@example.com",
14
+ * "name": "User Name",
15
+ * "deviceId": "device-uuid",
16
+ * "clientId": "client-uuid",
17
+ * "createdAt": "2024-01-01T00:00:00.000Z",
18
+ * "lastRefreshedAt": "2024-01-01T00:00:00.000Z"
19
+ * }
20
+ */
21
+
22
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
23
+ import { homedir } from 'node:os'
24
+ import { join } from 'node:path'
25
+
26
+ const MSTRO_DIR = join(homedir(), '.mstro')
27
+ const CREDENTIALS_FILE = join(MSTRO_DIR, 'credentials.json')
28
+
29
+ export interface Credentials {
30
+ token: string
31
+ userId: string
32
+ email: string
33
+ name?: string
34
+ deviceId?: string
35
+ clientId: string
36
+ createdAt: string
37
+ lastRefreshedAt?: string
38
+ }
39
+
40
+ /**
41
+ * Ensure the ~/.mstro directory exists
42
+ */
43
+ function ensureMstroDir(): void {
44
+ if (!existsSync(MSTRO_DIR)) {
45
+ mkdirSync(MSTRO_DIR, { recursive: true, mode: 0o700 })
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get stored credentials, or null if not logged in
51
+ */
52
+ export function getCredentials(): Credentials | null {
53
+ if (!existsSync(CREDENTIALS_FILE)) {
54
+ return null
55
+ }
56
+
57
+ try {
58
+ const content = readFileSync(CREDENTIALS_FILE, 'utf-8')
59
+ const credentials = JSON.parse(content) as Credentials
60
+
61
+ // Validate required fields
62
+ if (!credentials.token || !credentials.userId || !credentials.email || !credentials.clientId) {
63
+ console.warn('Invalid credentials file, missing required fields')
64
+ return null
65
+ }
66
+
67
+ return credentials
68
+ } catch (err) {
69
+ console.warn('Failed to read credentials file:', err)
70
+ return null
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Save credentials after successful login
76
+ */
77
+ export function saveCredentials(credentials: Credentials): void {
78
+ ensureMstroDir()
79
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
80
+ mode: 0o600 // Read/write for owner only
81
+ })
82
+ }
83
+
84
+ /**
85
+ * Update the token (used during refresh)
86
+ */
87
+ export function updateToken(newToken: string): void {
88
+ const credentials = getCredentials()
89
+ if (!credentials) {
90
+ throw new Error('No credentials to update')
91
+ }
92
+
93
+ credentials.token = newToken
94
+ credentials.lastRefreshedAt = new Date().toISOString()
95
+ saveCredentials(credentials)
96
+ }
97
+
98
+ /**
99
+ * Delete credentials (logout)
100
+ */
101
+ export function deleteCredentials(): boolean {
102
+ if (!existsSync(CREDENTIALS_FILE)) {
103
+ return false
104
+ }
105
+
106
+ try {
107
+ unlinkSync(CREDENTIALS_FILE)
108
+ return true
109
+ } catch (err) {
110
+ console.error('Failed to delete credentials:', err)
111
+ return false
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Check if user is logged in
117
+ */
118
+ export function isLoggedIn(): boolean {
119
+ return getCredentials() !== null
120
+ }
121
+
122
+ /**
123
+ * Get the credentials file path (for display)
124
+ */
125
+ export function getCredentialsPath(): string {
126
+ return CREDENTIALS_FILE
127
+ }
128
+
129
+ /**
130
+ * Get the mstro directory path
131
+ */
132
+ export function getMstroDir(): string {
133
+ return MSTRO_DIR
134
+ }