mstro-app 0.3.6 → 0.3.8
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/README.md +4 -8
- package/bin/mstro.js +54 -15
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +10 -6
- package/dist/server/cli/headless/claude-invoker.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 +20 -10
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +4 -1
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +8 -0
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +8 -1
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +45 -3
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/index.js +0 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +2 -0
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +55 -39
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/bouncer-sandbox.d.ts +60 -0
- package/dist/server/mcp/bouncer-sandbox.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-sandbox.js +182 -0
- package/dist/server/mcp/bouncer-sandbox.js.map +1 -0
- package/dist/server/mcp/security-patterns.d.ts +6 -12
- package/dist/server/mcp/security-patterns.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.js +197 -10
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/services/platform.d.ts +6 -4
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +30 -11
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +3 -1
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +0 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +7 -2
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts +4 -0
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-handlers.js +106 -0
- package/dist/server/services/websocket/quality-handlers.js.map +1 -0
- package/dist/server/services/websocket/quality-service.d.ts +54 -0
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-service.js +766 -0
- package/dist/server/services/websocket/quality-service.js.map +1 -0
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +23 -0
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +2 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/server/cli/headless/claude-invoker.ts +7 -5
- package/server/cli/headless/runner.ts +17 -4
- package/server/cli/headless/stall-assessor.ts +4 -1
- package/server/cli/headless/tool-watchdog.ts +8 -0
- package/server/cli/improvisation-session-manager.ts +50 -3
- package/server/index.ts +0 -4
- package/server/mcp/bouncer-integration.ts +66 -44
- package/server/mcp/bouncer-sandbox.ts +214 -0
- package/server/mcp/security-patterns.ts +206 -10
- package/server/services/platform.ts +29 -11
- package/server/services/terminal/pty-manager.ts +3 -1
- package/server/services/websocket/handler.ts +7 -2
- package/server/services/websocket/quality-handlers.ts +140 -0
- package/server/services/websocket/quality-service.ts +922 -0
- package/server/services/websocket/session-handlers.ts +26 -0
- package/server/services/websocket/types.ts +14 -0
|
@@ -117,6 +117,9 @@ interface ConnectionCallbacks {
|
|
|
117
117
|
/**
|
|
118
118
|
* Platform WebSocket connection with token-based authentication
|
|
119
119
|
*/
|
|
120
|
+
/** Number of missed pongs before treating connection as dead */
|
|
121
|
+
const MAX_MISSED_PONGS = 2
|
|
122
|
+
|
|
120
123
|
export class PlatformConnection {
|
|
121
124
|
private ws: WebSocket | null = null
|
|
122
125
|
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null
|
|
@@ -130,6 +133,7 @@ export class PlatformConnection {
|
|
|
130
133
|
private isConnected = false
|
|
131
134
|
private tokenRefreshInterval: ReturnType<typeof setInterval> | null = null
|
|
132
135
|
private heartbeatInterval: ReturnType<typeof setInterval> | null = null
|
|
136
|
+
private missedPongs = 0
|
|
133
137
|
|
|
134
138
|
constructor(
|
|
135
139
|
workingDirectory: string,
|
|
@@ -184,19 +188,33 @@ export class PlatformConnection {
|
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
/**
|
|
187
|
-
* Start heartbeat to keep connection alive and refresh server-side TTL
|
|
191
|
+
* Start heartbeat to keep connection alive and refresh server-side TTL.
|
|
192
|
+
* Tracks missed pongs — if the server doesn't respond to MAX_MISSED_PONGS
|
|
193
|
+
* consecutive pings, the connection is considered dead and force-closed
|
|
194
|
+
* to trigger reconnection.
|
|
188
195
|
*/
|
|
189
196
|
private startHeartbeat(): void {
|
|
197
|
+
this.missedPongs = 0
|
|
190
198
|
// Send ping every 2 minutes (server TTL is 5 minutes)
|
|
191
|
-
this.heartbeatInterval = setInterval(() =>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
199
|
+
this.heartbeatInterval = setInterval(() => this.heartbeatTick(), 2 * 60 * 1000)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private heartbeatTick(): void {
|
|
203
|
+
if (!this.ws || !this.isConnected) return
|
|
204
|
+
|
|
205
|
+
if (this.missedPongs >= MAX_MISSED_PONGS) {
|
|
206
|
+
console.log(`[Platform] ${this.missedPongs} pongs missed — forcing reconnect`)
|
|
207
|
+
this.missedPongs = 0
|
|
208
|
+
this.stopHeartbeat()
|
|
209
|
+
try { this.ws.close() } catch { /* ignore */ }
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
this.missedPongs++
|
|
213
|
+
try {
|
|
214
|
+
this.ws.send(JSON.stringify({ type: 'ping' }))
|
|
215
|
+
} catch {
|
|
216
|
+
// Send failed — onclose will handle reconnect
|
|
217
|
+
}
|
|
200
218
|
}
|
|
201
219
|
|
|
202
220
|
/**
|
|
@@ -363,7 +381,7 @@ export class PlatformConnection {
|
|
|
363
381
|
break
|
|
364
382
|
|
|
365
383
|
case 'pong':
|
|
366
|
-
|
|
384
|
+
this.missedPongs = 0
|
|
367
385
|
break
|
|
368
386
|
|
|
369
387
|
default:
|
|
@@ -312,7 +312,9 @@ export class PTYManager extends EventEmitter {
|
|
|
312
312
|
// wraps echoed chars in multi-part ANSI sequences (RPROMPT, syntax highlighting).
|
|
313
313
|
// A longer window on macOS ensures these multi-part sequences arrive as one chunk,
|
|
314
314
|
// which the browser's predictive echo can match correctly.
|
|
315
|
-
|
|
315
|
+
// 32ms on macOS captures full zsh redraw cycles (RPROMPT + syntax highlighting)
|
|
316
|
+
// that 24ms often split across coalesce boundaries.
|
|
317
|
+
const OUTPUT_COALESCE_MS = platform() === 'darwin' ? 32 : 8;
|
|
316
318
|
// High-water mark: flush immediately when buffer exceeds this size
|
|
317
319
|
// to prevent unbounded memory growth during high-output commands (e.g. `yes`)
|
|
318
320
|
const OUTPUT_HIGH_WATER = 64 * 1024; // 64KB
|
|
@@ -19,6 +19,7 @@ import { handleFileExplorerMessage, handleFileMessage } from './file-explorer-ha
|
|
|
19
19
|
import { FileUploadHandler } from './file-upload-handler.js';
|
|
20
20
|
import { handleGitMessage } from './git-handlers.js';
|
|
21
21
|
import type { HandlerContext, UsageReporter } from './handler-context.js';
|
|
22
|
+
import { handleQualityMessage } from './quality-handlers.js';
|
|
22
23
|
import { handleHistoryMessage, handleSessionMessage, initializeTab, resumeHistoricalSession } from './session-handlers.js';
|
|
23
24
|
import { SessionRegistry } from './session-registry.js';
|
|
24
25
|
import { generateNotificationSummary, handleGetSettings, handleUpdateSettings } from './settings-handlers.js';
|
|
@@ -220,6 +221,12 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
220
221
|
return handleRemoveTab(this, ws, tabId, workingDir);
|
|
221
222
|
case 'markTabViewed':
|
|
222
223
|
return handleMarkTabViewed(this, ws, tabId, workingDir);
|
|
224
|
+
// Quality messages
|
|
225
|
+
case 'qualityDetectTools':
|
|
226
|
+
case 'qualityScan':
|
|
227
|
+
case 'qualityInstallTools':
|
|
228
|
+
case 'qualityCodeReview':
|
|
229
|
+
return handleQualityMessage(this, ws, msg, tabId, workingDir);
|
|
223
230
|
// Settings messages
|
|
224
231
|
case 'getSettings':
|
|
225
232
|
return handleGetSettings(this, ws);
|
|
@@ -291,6 +298,4 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
291
298
|
this.sessions.delete(sessionId);
|
|
292
299
|
}
|
|
293
300
|
|
|
294
|
-
cleanupStaleSessions(): void {
|
|
295
|
-
}
|
|
296
301
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import type { HandlerContext } from './handler-context.js';
|
|
6
|
+
import { detectTools, installTools, runQualityScan } from './quality-service.js';
|
|
7
|
+
import type { WebSocketMessage, WSContext } from './types.js';
|
|
8
|
+
|
|
9
|
+
export function handleQualityMessage(
|
|
10
|
+
ctx: HandlerContext,
|
|
11
|
+
ws: WSContext,
|
|
12
|
+
msg: WebSocketMessage,
|
|
13
|
+
_tabId: string,
|
|
14
|
+
workingDir: string,
|
|
15
|
+
): void {
|
|
16
|
+
const handlers: Record<string, () => void> = {
|
|
17
|
+
qualityDetectTools: () => handleDetectTools(ctx, ws, msg, workingDir),
|
|
18
|
+
qualityScan: () => handleScan(ctx, ws, msg, workingDir),
|
|
19
|
+
qualityInstallTools: () => handleInstallTools(ctx, ws, msg, workingDir),
|
|
20
|
+
qualityCodeReview: () => handleCodeReview(ctx, ws, msg, workingDir),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const handler = handlers[msg.type];
|
|
24
|
+
if (!handler) return;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
handler();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
30
|
+
ctx.send(ws, {
|
|
31
|
+
type: 'qualityError',
|
|
32
|
+
data: { path: msg.data?.path || workingDir, error: errMsg },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolvePath(workingDir: string, dirPath?: string): string {
|
|
38
|
+
if (!dirPath || dirPath === '.' || dirPath === './') return workingDir;
|
|
39
|
+
if (dirPath.startsWith('/')) return dirPath;
|
|
40
|
+
return join(workingDir, dirPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function handleDetectTools(
|
|
44
|
+
ctx: HandlerContext,
|
|
45
|
+
ws: WSContext,
|
|
46
|
+
msg: WebSocketMessage,
|
|
47
|
+
workingDir: string,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const dirPath = resolvePath(workingDir, msg.data?.path);
|
|
50
|
+
try {
|
|
51
|
+
const { tools, ecosystem } = await detectTools(dirPath);
|
|
52
|
+
ctx.send(ws, {
|
|
53
|
+
type: 'qualityToolsDetected',
|
|
54
|
+
data: { path: msg.data?.path || '.', tools, ecosystem },
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
ctx.send(ws, {
|
|
58
|
+
type: 'qualityError',
|
|
59
|
+
data: { path: msg.data?.path || '.', error: error instanceof Error ? error.message : String(error) },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function handleScan(
|
|
65
|
+
ctx: HandlerContext,
|
|
66
|
+
ws: WSContext,
|
|
67
|
+
msg: WebSocketMessage,
|
|
68
|
+
workingDir: string,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const dirPath = resolvePath(workingDir, msg.data?.path);
|
|
71
|
+
const reportPath = msg.data?.path || '.';
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const results = await runQualityScan(dirPath, (progress) => {
|
|
75
|
+
ctx.send(ws, {
|
|
76
|
+
type: 'qualityScanProgress',
|
|
77
|
+
data: { path: reportPath, progress },
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
ctx.send(ws, {
|
|
81
|
+
type: 'qualityScanResults',
|
|
82
|
+
data: { path: reportPath, results },
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
ctx.send(ws, {
|
|
86
|
+
type: 'qualityError',
|
|
87
|
+
data: { path: reportPath, error: error instanceof Error ? error.message : String(error) },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function handleInstallTools(
|
|
93
|
+
ctx: HandlerContext,
|
|
94
|
+
ws: WSContext,
|
|
95
|
+
msg: WebSocketMessage,
|
|
96
|
+
workingDir: string,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const dirPath = resolvePath(workingDir, msg.data?.path);
|
|
99
|
+
const reportPath = msg.data?.path || '.';
|
|
100
|
+
const toolNames: string[] | undefined = msg.data?.tools;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
ctx.send(ws, {
|
|
104
|
+
type: 'qualityInstallProgress',
|
|
105
|
+
data: { path: reportPath, installing: true },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const { tools, ecosystem } = await installTools(dirPath, toolNames);
|
|
109
|
+
|
|
110
|
+
ctx.send(ws, {
|
|
111
|
+
type: 'qualityInstallComplete',
|
|
112
|
+
data: { path: reportPath, tools, ecosystem },
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
ctx.send(ws, {
|
|
116
|
+
type: 'qualityError',
|
|
117
|
+
data: { path: reportPath, error: error instanceof Error ? error.message : String(error) },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function handleCodeReview(
|
|
123
|
+
ctx: HandlerContext,
|
|
124
|
+
ws: WSContext,
|
|
125
|
+
msg: WebSocketMessage,
|
|
126
|
+
_workingDir: string,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const reportPath = msg.data?.path || '.';
|
|
129
|
+
|
|
130
|
+
// Code review via headless Claude will be implemented in a follow-up.
|
|
131
|
+
// For now, send an empty result to unblock the UI.
|
|
132
|
+
ctx.send(ws, {
|
|
133
|
+
type: 'qualityCodeReview',
|
|
134
|
+
data: {
|
|
135
|
+
path: reportPath,
|
|
136
|
+
findings: [],
|
|
137
|
+
summary: 'Code review requires a Claude Code session. Run from the Chat view for a detailed code review.',
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|