mstro-app 0.2.0 → 0.3.1
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/PRIVACY.md +126 -0
- package/README.md +24 -23
- package/bin/commands/login.js +79 -49
- package/bin/mstro.js +305 -39
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +137 -30
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +2 -2
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts +6 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +59 -4
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +3 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +20 -1
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +4 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +30 -24
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +20 -2
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +30 -3
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +224 -31
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/index.js +6 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.js +53 -14
- package/dist/server/mcp/bouncer-cli.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +70 -7
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-audit.d.ts +3 -3
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-audit.js.map +1 -1
- package/dist/server/mcp/server.js +3 -2
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/services/analytics.d.ts +2 -2
- package/dist/server/services/analytics.d.ts.map +1 -1
- package/dist/server/services/analytics.js +13 -1
- package/dist/server/services/analytics.js.map +1 -1
- package/dist/server/services/files.js +7 -7
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/pathUtils.js +1 -1
- package/dist/server/services/pathUtils.js.map +1 -1
- package/dist/server/services/platform.d.ts +2 -2
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +13 -1
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/sentry.d.ts +1 -1
- package/dist/server/services/sentry.d.ts.map +1 -1
- package/dist/server/services/sentry.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +12 -0
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +81 -6
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.d.ts +5 -0
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +518 -0
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-utils.d.ts +4 -0
- package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
- package/dist/server/services/websocket/file-utils.js +27 -8
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-handlers.d.ts +36 -0
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-handlers.js +797 -0
- package/dist/server/services/websocket/git-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts +4 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.js +299 -0
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts +4 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.js +353 -0
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -0
- package/dist/server/services/websocket/handler-context.d.ts +32 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -0
- package/dist/server/services/websocket/handler-context.js +4 -0
- package/dist/server/services/websocket/handler-context.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +27 -359
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +68 -2329
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/index.d.ts +1 -1
- package/dist/server/services/websocket/index.d.ts.map +1 -1
- package/dist/server/services/websocket/index.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +10 -0
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/session-handlers.js +508 -0
- package/dist/server/services/websocket/session-handlers.js.map +1 -0
- package/dist/server/services/websocket/settings-handlers.d.ts +6 -0
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/settings-handlers.js +125 -0
- package/dist/server/services/websocket/settings-handlers.js.map +1 -0
- package/dist/server/services/websocket/tab-handlers.d.ts +10 -0
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-handlers.js +131 -0
- package/dist/server/services/websocket/tab-handlers.js.map +1 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts +9 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/terminal-handlers.js +220 -0
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +63 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.d.ts +22 -2
- package/dist/server/utils/agent-manager.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.js +2 -2
- package/dist/server/utils/agent-manager.js.map +1 -1
- package/dist/server/utils/port-manager.js.map +1 -1
- package/hooks/bouncer.sh +17 -3
- package/package.json +7 -3
- package/server/README.md +176 -159
- package/server/cli/headless/claude-invoker.ts +172 -43
- package/server/cli/headless/mcp-config.ts +8 -8
- package/server/cli/headless/runner.ts +57 -4
- package/server/cli/headless/stall-assessor.ts +25 -0
- package/server/cli/headless/tool-watchdog.ts +33 -25
- package/server/cli/headless/types.ts +11 -2
- package/server/cli/improvisation-session-manager.ts +285 -37
- package/server/index.ts +15 -13
- package/server/mcp/README.md +59 -67
- package/server/mcp/bouncer-cli.ts +73 -20
- package/server/mcp/bouncer-integration.ts +99 -16
- package/server/mcp/security-audit.ts +4 -4
- package/server/mcp/server.ts +6 -5
- package/server/services/analytics.ts +16 -4
- package/server/services/files.ts +13 -13
- package/server/services/pathUtils.ts +2 -2
- package/server/services/platform.ts +17 -6
- package/server/services/sentry.ts +1 -1
- package/server/services/terminal/pty-manager.ts +88 -11
- package/server/services/websocket/file-explorer-handlers.ts +587 -0
- package/server/services/websocket/file-utils.ts +28 -9
- package/server/services/websocket/git-handlers.ts +924 -0
- package/server/services/websocket/git-pr-handlers.ts +363 -0
- package/server/services/websocket/git-worktree-handlers.ts +403 -0
- package/server/services/websocket/handler-context.ts +44 -0
- package/server/services/websocket/handler.ts +85 -2680
- package/server/services/websocket/index.ts +1 -1
- package/server/services/websocket/session-handlers.ts +575 -0
- package/server/services/websocket/settings-handlers.ts +150 -0
- package/server/services/websocket/tab-handlers.ts +150 -0
- package/server/services/websocket/terminal-handlers.ts +277 -0
- package/server/services/websocket/types.ts +137 -0
- package/server/utils/agent-manager.ts +6 -6
- package/server/utils/port-manager.ts +1 -1
- package/bin/release.sh +0 -110
- package/server/services/platform.test.ts +0 -1304
- package/server/services/websocket/handler.test.ts +0 -20
package/server/mcp/server.ts
CHANGED
|
@@ -73,7 +73,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
73
73
|
|
|
74
74
|
const { tool_name, input } = request.params.arguments as {
|
|
75
75
|
tool_name: string;
|
|
76
|
-
input: Record<string,
|
|
76
|
+
input: Record<string, unknown>;
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
console.error(`[MCP Bouncer] Analyzing ${tool_name} request...`);
|
|
@@ -84,7 +84,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
84
84
|
|
|
85
85
|
// Extract file path with multiple property name support
|
|
86
86
|
// Claude Code may use file_path, filePath, or path depending on context
|
|
87
|
-
const getFilePath = (inp: Record<string,
|
|
87
|
+
const getFilePath = (inp: Record<string, unknown>) =>
|
|
88
88
|
inp.file_path || inp.filePath || inp.path;
|
|
89
89
|
|
|
90
90
|
if (tool_name === 'Bash' && input.command) {
|
|
@@ -141,8 +141,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
141
141
|
},
|
|
142
142
|
],
|
|
143
143
|
};
|
|
144
|
-
} catch (error:
|
|
145
|
-
|
|
144
|
+
} catch (error: unknown) {
|
|
145
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
146
|
+
console.error(`[MCP Bouncer] Error: ${errorMessage}`);
|
|
146
147
|
|
|
147
148
|
// Fail-safe: deny on error
|
|
148
149
|
return {
|
|
@@ -151,7 +152,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
151
152
|
type: 'text',
|
|
152
153
|
text: JSON.stringify({
|
|
153
154
|
behavior: 'deny',
|
|
154
|
-
message: `Security analysis failed: ${
|
|
155
|
+
message: `Security analysis failed: ${errorMessage}. Denying for safety.`,
|
|
155
156
|
}),
|
|
156
157
|
},
|
|
157
158
|
],
|
|
@@ -23,7 +23,19 @@ import { getClientId } from './client-id.js'
|
|
|
23
23
|
|
|
24
24
|
const MSTRO_DIR = join(homedir(), '.mstro')
|
|
25
25
|
const CONFIG_FILE = join(MSTRO_DIR, 'config.json')
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
// Read SERVER_URL from ~/.mstro/.env if it exists (for local dev)
|
|
28
|
+
function getServerUrl(): string {
|
|
29
|
+
try {
|
|
30
|
+
const envPath = join(MSTRO_DIR, '.env')
|
|
31
|
+
const content = readFileSync(envPath, 'utf-8')
|
|
32
|
+
const match = content.match(/^SERVER_URL=(.+)$/m)
|
|
33
|
+
if (match) return match[1].trim()
|
|
34
|
+
} catch {}
|
|
35
|
+
return 'https://api.mstro.app'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PLATFORM_URL = process.env.PLATFORM_URL || getServerUrl()
|
|
27
39
|
|
|
28
40
|
let client: PostHog | null = null
|
|
29
41
|
let telemetryEnabled: boolean | null = null
|
|
@@ -146,7 +158,7 @@ function getDistinctId(): string {
|
|
|
146
158
|
/**
|
|
147
159
|
* Get common properties included with all events
|
|
148
160
|
*/
|
|
149
|
-
function getCommonProperties(): Record<string,
|
|
161
|
+
function getCommonProperties(): Record<string, unknown> {
|
|
150
162
|
return {
|
|
151
163
|
os: platform(),
|
|
152
164
|
arch: arch(),
|
|
@@ -159,7 +171,7 @@ function getCommonProperties(): Record<string, any> {
|
|
|
159
171
|
/**
|
|
160
172
|
* Track a custom event
|
|
161
173
|
*/
|
|
162
|
-
export function trackEvent(event: string, properties?: Record<string,
|
|
174
|
+
export function trackEvent(event: string, properties?: Record<string, unknown>): void {
|
|
163
175
|
if (!client || !isTelemetryEnabled()) return
|
|
164
176
|
|
|
165
177
|
client.capture({
|
|
@@ -175,7 +187,7 @@ export function trackEvent(event: string, properties?: Record<string, any>): voi
|
|
|
175
187
|
/**
|
|
176
188
|
* Identify a user (call after login)
|
|
177
189
|
*/
|
|
178
|
-
export function identifyUser(userId: string, properties?: Record<string,
|
|
190
|
+
export function identifyUser(userId: string, properties?: Record<string, unknown>): void {
|
|
179
191
|
if (!client || !isTelemetryEnabled()) return
|
|
180
192
|
|
|
181
193
|
// Link the client ID to the user ID
|
package/server/services/files.ts
CHANGED
|
@@ -332,9 +332,9 @@ export function listDirectory(
|
|
|
332
332
|
success: true,
|
|
333
333
|
entries: directoryEntries
|
|
334
334
|
}
|
|
335
|
-
} catch (error:
|
|
335
|
+
} catch (error: unknown) {
|
|
336
336
|
// Handle permission errors gracefully
|
|
337
|
-
if (error.code === 'EACCES') {
|
|
337
|
+
if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'EACCES') {
|
|
338
338
|
return {
|
|
339
339
|
success: false,
|
|
340
340
|
error: 'Permission denied'
|
|
@@ -344,7 +344,7 @@ export function listDirectory(
|
|
|
344
344
|
console.error('[FileService] Error listing directory:', error)
|
|
345
345
|
return {
|
|
346
346
|
success: false,
|
|
347
|
-
error: error.message
|
|
347
|
+
error: error instanceof Error ? error.message : 'Failed to list directory'
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
}
|
|
@@ -408,11 +408,11 @@ export function writeFile(
|
|
|
408
408
|
success: true,
|
|
409
409
|
path: resolvedPath.replace(`${workingDir}/`, '')
|
|
410
410
|
}
|
|
411
|
-
} catch (error:
|
|
411
|
+
} catch (error: unknown) {
|
|
412
412
|
console.error('[FileService] Error writing file:', error)
|
|
413
413
|
return {
|
|
414
414
|
success: false,
|
|
415
|
-
error: error.message
|
|
415
|
+
error: error instanceof Error ? error.message : 'Failed to write file'
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
}
|
|
@@ -471,11 +471,11 @@ export function createFile(
|
|
|
471
471
|
success: true,
|
|
472
472
|
path: resolvedPath.replace(`${workingDir}/`, '')
|
|
473
473
|
}
|
|
474
|
-
} catch (error:
|
|
474
|
+
} catch (error: unknown) {
|
|
475
475
|
console.error('[FileService] Error creating file:', error)
|
|
476
476
|
return {
|
|
477
477
|
success: false,
|
|
478
|
-
error: error.message
|
|
478
|
+
error: error instanceof Error ? error.message : 'Failed to create file'
|
|
479
479
|
}
|
|
480
480
|
}
|
|
481
481
|
}
|
|
@@ -536,11 +536,11 @@ export function createDirectory(
|
|
|
536
536
|
success: true,
|
|
537
537
|
path: resolvedPath.replace(`${workingDir}/`, '')
|
|
538
538
|
}
|
|
539
|
-
} catch (error:
|
|
539
|
+
} catch (error: unknown) {
|
|
540
540
|
console.error('[FileService] Error creating directory:', error)
|
|
541
541
|
return {
|
|
542
542
|
success: false,
|
|
543
|
-
error: error.message
|
|
543
|
+
error: error instanceof Error ? error.message : 'Failed to create directory'
|
|
544
544
|
}
|
|
545
545
|
}
|
|
546
546
|
}
|
|
@@ -618,11 +618,11 @@ export function deleteFile(
|
|
|
618
618
|
success: true,
|
|
619
619
|
path: resolvedPath.replace(`${workingDir}/`, '')
|
|
620
620
|
}
|
|
621
|
-
} catch (error:
|
|
621
|
+
} catch (error: unknown) {
|
|
622
622
|
console.error('[FileService] Error deleting file:', error)
|
|
623
623
|
return {
|
|
624
624
|
success: false,
|
|
625
|
-
error: error.message
|
|
625
|
+
error: error instanceof Error ? error.message : 'Failed to delete'
|
|
626
626
|
}
|
|
627
627
|
}
|
|
628
628
|
}
|
|
@@ -700,11 +700,11 @@ export function renameFile(
|
|
|
700
700
|
success: true,
|
|
701
701
|
path: resolvedNewPath.replace(`${workingDir}/`, '')
|
|
702
702
|
}
|
|
703
|
-
} catch (error:
|
|
703
|
+
} catch (error: unknown) {
|
|
704
704
|
console.error('[FileService] Error renaming file:', error)
|
|
705
705
|
return {
|
|
706
706
|
success: false,
|
|
707
|
-
error: error.message
|
|
707
|
+
error: error instanceof Error ? error.message : 'Failed to rename'
|
|
708
708
|
}
|
|
709
709
|
}
|
|
710
710
|
}
|
|
@@ -71,12 +71,12 @@ export function validatePathWithinWorkingDir(
|
|
|
71
71
|
valid: true,
|
|
72
72
|
resolvedPath
|
|
73
73
|
};
|
|
74
|
-
} catch (error:
|
|
74
|
+
} catch (error: unknown) {
|
|
75
75
|
console.error('[PathUtils] Error validating path:', error);
|
|
76
76
|
return {
|
|
77
77
|
valid: false,
|
|
78
78
|
resolvedPath: '',
|
|
79
|
-
error: `Invalid path: ${error.message}`
|
|
79
|
+
error: `Invalid path: ${error instanceof Error ? error.message : String(error)}`
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -102,7 +102,18 @@ if (typeof WebSocket !== 'undefined') {
|
|
|
102
102
|
WebSocketImpl = WS as unknown as typeof WebSocket
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
// Read SERVER_URL from ~/.mstro/.env if it exists (for local dev)
|
|
106
|
+
function getServerUrl(): string {
|
|
107
|
+
try {
|
|
108
|
+
const envPath = join(MSTRO_DIR, '.env')
|
|
109
|
+
const content = readFileSync(envPath, 'utf-8')
|
|
110
|
+
const match = content.match(/^SERVER_URL=(.+)$/m)
|
|
111
|
+
if (match) return match[1].trim()
|
|
112
|
+
} catch {}
|
|
113
|
+
return 'https://api.mstro.app'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const DEFAULT_PLATFORM_URL = process.env.PLATFORM_URL || getServerUrl()
|
|
106
117
|
|
|
107
118
|
interface ConnectionCallbacks {
|
|
108
119
|
onConnected?: (connectionId: string) => void
|
|
@@ -110,7 +121,7 @@ interface ConnectionCallbacks {
|
|
|
110
121
|
onError?: (error: string) => void
|
|
111
122
|
onWebConnected?: () => void
|
|
112
123
|
onWebDisconnected?: () => void
|
|
113
|
-
onRelayedMessage?: (message:
|
|
124
|
+
onRelayedMessage?: (message: unknown) => void
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
/**
|
|
@@ -340,15 +351,15 @@ export class PlatformConnection {
|
|
|
340
351
|
}
|
|
341
352
|
}
|
|
342
353
|
|
|
343
|
-
private handleMessage(message:
|
|
354
|
+
private handleMessage(message: Record<string, unknown>): void {
|
|
344
355
|
switch (message.type) {
|
|
345
356
|
case 'paired':
|
|
346
357
|
this.isConnected = true
|
|
347
|
-
this.connectionId = message.connectionId
|
|
358
|
+
this.connectionId = message.connectionId as string
|
|
348
359
|
// Connection status printed by onConnected callback
|
|
349
360
|
// Start heartbeat to keep server-side TTL refreshed
|
|
350
361
|
this.startHeartbeat()
|
|
351
|
-
this.callbacks.onConnected?.(message.connectionId)
|
|
362
|
+
this.callbacks.onConnected?.(message.connectionId as string)
|
|
352
363
|
break
|
|
353
364
|
|
|
354
365
|
case 'web_connected':
|
|
@@ -393,7 +404,7 @@ export class PlatformConnection {
|
|
|
393
404
|
/**
|
|
394
405
|
* Send message to platform (will be relayed to web if connected)
|
|
395
406
|
*/
|
|
396
|
-
send(message:
|
|
407
|
+
send(message: unknown): void {
|
|
397
408
|
if (this.ws && this.ws.readyState === WebSocketImpl.OPEN) {
|
|
398
409
|
this.ws.send(JSON.stringify(message))
|
|
399
410
|
}
|
|
@@ -65,7 +65,7 @@ export function initSentry(): void {
|
|
|
65
65
|
})
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export function captureException(error: unknown, context?: Record<string,
|
|
68
|
+
export function captureException(error: unknown, context?: Record<string, unknown>): void {
|
|
69
69
|
if (!initialized) return
|
|
70
70
|
Sentry.captureException(error, context ? { extra: context } : undefined)
|
|
71
71
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { EventEmitter } from 'node:events';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
20
21
|
import { homedir, platform } from 'node:os';
|
|
21
22
|
import { sanitizeEnvForSandbox } from '../sandbox-utils.js';
|
|
22
23
|
|
|
@@ -26,8 +27,8 @@ let _ptyLoadError: string | null = null;
|
|
|
26
27
|
|
|
27
28
|
try {
|
|
28
29
|
pty = await import('node-pty');
|
|
29
|
-
} catch (error:
|
|
30
|
-
_ptyLoadError = error.message
|
|
30
|
+
} catch (error: unknown) {
|
|
31
|
+
_ptyLoadError = error instanceof Error ? error.message : 'Failed to load node-pty';
|
|
31
32
|
console.warn('[PTYManager] node-pty not available - terminal features disabled');
|
|
32
33
|
console.warn('[PTYManager] To enable terminals, run: mstro setup-terminal');
|
|
33
34
|
}
|
|
@@ -39,6 +40,32 @@ export function isPtyAvailable(): boolean {
|
|
|
39
40
|
return pty !== null;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Re-attempt loading node-pty at runtime.
|
|
45
|
+
* Called after `mstro setup-terminal` compiles the native module
|
|
46
|
+
* so the running server can pick it up without a restart.
|
|
47
|
+
*
|
|
48
|
+
* Uses createRequire (CJS) to bypass ESM's module cache — a failed
|
|
49
|
+
* ESM import is permanently cached, but CJS require cache entries
|
|
50
|
+
* can be deleted and re-required.
|
|
51
|
+
*/
|
|
52
|
+
export async function reloadPty(): Promise<boolean> {
|
|
53
|
+
if (pty) return true;
|
|
54
|
+
try {
|
|
55
|
+
const require = createRequire(import.meta.url);
|
|
56
|
+
// Clear any cached failure so require() retries the native load
|
|
57
|
+
const resolved = require.resolve('node-pty');
|
|
58
|
+
delete require.cache[resolved];
|
|
59
|
+
pty = require('node-pty');
|
|
60
|
+
_ptyLoadError = null;
|
|
61
|
+
console.log('[PTYManager] node-pty loaded successfully after reload');
|
|
62
|
+
return true;
|
|
63
|
+
} catch (error: unknown) {
|
|
64
|
+
_ptyLoadError = error instanceof Error ? error.message : 'Failed to load node-pty';
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
42
69
|
/**
|
|
43
70
|
* Get installation instructions for node-pty based on platform
|
|
44
71
|
*/
|
|
@@ -87,6 +114,9 @@ export interface PTYSession {
|
|
|
87
114
|
// Current dimensions
|
|
88
115
|
cols: number;
|
|
89
116
|
rows: number;
|
|
117
|
+
// Output coalescing: buffer small chunks into fewer WS messages
|
|
118
|
+
_outputBuffer: string;
|
|
119
|
+
_outputTimer: ReturnType<typeof setTimeout> | null;
|
|
90
120
|
}
|
|
91
121
|
|
|
92
122
|
/**
|
|
@@ -227,25 +257,63 @@ export class PTYManager extends EventEmitter {
|
|
|
227
257
|
lastActivityAt: Date.now(),
|
|
228
258
|
cols,
|
|
229
259
|
rows,
|
|
260
|
+
_outputBuffer: '',
|
|
261
|
+
_outputTimer: null,
|
|
230
262
|
};
|
|
231
263
|
this.terminals.set(terminalId, session);
|
|
232
264
|
|
|
233
|
-
// Handle data output
|
|
265
|
+
// Handle data output — coalesce small chunks to reduce WebSocket message count.
|
|
266
|
+
// On macOS, node-pty emits many tiny chunks (sometimes single bytes) and zsh
|
|
267
|
+
// wraps echoed chars in multi-part ANSI sequences (RPROMPT, syntax highlighting).
|
|
268
|
+
// A longer window on macOS ensures these multi-part sequences arrive as one chunk,
|
|
269
|
+
// which the browser's predictive echo can match correctly.
|
|
270
|
+
const OUTPUT_COALESCE_MS = platform() === 'darwin' ? 24 : 8;
|
|
271
|
+
// High-water mark: flush immediately when buffer exceeds this size
|
|
272
|
+
// to prevent unbounded memory growth during high-output commands (e.g. `yes`)
|
|
273
|
+
const OUTPUT_HIGH_WATER = 64 * 1024; // 64KB
|
|
274
|
+
// Maximum chunk size per WebSocket message to prevent browser overload
|
|
275
|
+
const OUTPUT_CHUNK_SIZE = 64 * 1024;
|
|
276
|
+
|
|
277
|
+
const flushOutputBuffer = () => {
|
|
278
|
+
if (session._outputTimer) {
|
|
279
|
+
clearTimeout(session._outputTimer);
|
|
280
|
+
session._outputTimer = null;
|
|
281
|
+
}
|
|
282
|
+
const buffered = session._outputBuffer;
|
|
283
|
+
session._outputBuffer = '';
|
|
284
|
+
// Chunk large output to prevent single massive WebSocket frames
|
|
285
|
+
for (let i = 0; i < buffered.length; i += OUTPUT_CHUNK_SIZE) {
|
|
286
|
+
this.emit('output', terminalId, buffered.slice(i, i + OUTPUT_CHUNK_SIZE));
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
234
290
|
ptyProcess.onData((data: string) => {
|
|
235
291
|
session.lastActivityAt = Date.now();
|
|
236
|
-
|
|
292
|
+
session._outputBuffer += data;
|
|
293
|
+
// Flush immediately if buffer exceeds high-water mark
|
|
294
|
+
if (session._outputBuffer.length >= OUTPUT_HIGH_WATER) {
|
|
295
|
+
flushOutputBuffer();
|
|
296
|
+
} else if (!session._outputTimer) {
|
|
297
|
+
session._outputTimer = setTimeout(flushOutputBuffer, OUTPUT_COALESCE_MS);
|
|
298
|
+
}
|
|
237
299
|
});
|
|
238
300
|
|
|
239
|
-
// Handle exit
|
|
301
|
+
// Handle exit — flush any buffered output first
|
|
240
302
|
ptyProcess.onExit(({ exitCode }) => {
|
|
303
|
+
if (session._outputBuffer) {
|
|
304
|
+
flushOutputBuffer();
|
|
305
|
+
} else if (session._outputTimer) {
|
|
306
|
+
clearTimeout(session._outputTimer);
|
|
307
|
+
session._outputTimer = null;
|
|
308
|
+
}
|
|
241
309
|
this.emit('exit', terminalId, exitCode);
|
|
242
310
|
this.terminals.delete(terminalId);
|
|
243
311
|
});
|
|
244
312
|
|
|
245
313
|
return { shell: session.shell, cwd, isReconnect: false };
|
|
246
|
-
} catch (error:
|
|
314
|
+
} catch (error: unknown) {
|
|
247
315
|
console.error(`[PTYManager] Failed to create terminal ${terminalId}:`, error);
|
|
248
|
-
this.emit('error', terminalId, error.message
|
|
316
|
+
this.emit('error', terminalId, error instanceof Error ? error.message : 'Failed to create terminal');
|
|
249
317
|
throw error;
|
|
250
318
|
}
|
|
251
319
|
}
|
|
@@ -263,9 +331,9 @@ export class PTYManager extends EventEmitter {
|
|
|
263
331
|
try {
|
|
264
332
|
session.pty.write(data);
|
|
265
333
|
return true;
|
|
266
|
-
} catch (error:
|
|
334
|
+
} catch (error: unknown) {
|
|
267
335
|
console.error(`[PTYManager] Error writing to terminal ${terminalId}:`, error);
|
|
268
|
-
this.emit('error', terminalId, error.message
|
|
336
|
+
this.emit('error', terminalId, error instanceof Error ? error.message : 'Write failed');
|
|
269
337
|
return false;
|
|
270
338
|
}
|
|
271
339
|
}
|
|
@@ -283,7 +351,7 @@ export class PTYManager extends EventEmitter {
|
|
|
283
351
|
try {
|
|
284
352
|
session.pty.resize(cols, rows);
|
|
285
353
|
return true;
|
|
286
|
-
} catch (error:
|
|
354
|
+
} catch (error: unknown) {
|
|
287
355
|
console.error(`[PTYManager] Error resizing terminal ${terminalId}:`, error);
|
|
288
356
|
return false;
|
|
289
357
|
}
|
|
@@ -300,10 +368,19 @@ export class PTYManager extends EventEmitter {
|
|
|
300
368
|
|
|
301
369
|
|
|
302
370
|
try {
|
|
371
|
+
// Flush any coalesced output before closing
|
|
372
|
+
if (session._outputTimer) {
|
|
373
|
+
clearTimeout(session._outputTimer);
|
|
374
|
+
if (session._outputBuffer) {
|
|
375
|
+
this.emit('output', terminalId, session._outputBuffer);
|
|
376
|
+
session._outputBuffer = '';
|
|
377
|
+
}
|
|
378
|
+
session._outputTimer = null;
|
|
379
|
+
}
|
|
303
380
|
session.pty.kill();
|
|
304
381
|
this.terminals.delete(terminalId);
|
|
305
382
|
return true;
|
|
306
|
-
} catch (error:
|
|
383
|
+
} catch (error: unknown) {
|
|
307
384
|
console.error(`[PTYManager] Error closing terminal ${terminalId}:`, error);
|
|
308
385
|
this.terminals.delete(terminalId);
|
|
309
386
|
return false;
|