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.
Files changed (74) hide show
  1. package/README.md +4 -8
  2. package/bin/mstro.js +54 -15
  3. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  4. package/dist/server/cli/headless/claude-invoker.js +10 -6
  5. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  6. package/dist/server/cli/headless/runner.d.ts +6 -1
  7. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  8. package/dist/server/cli/headless/runner.js +20 -10
  9. package/dist/server/cli/headless/runner.js.map +1 -1
  10. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  11. package/dist/server/cli/headless/stall-assessor.js +4 -1
  12. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  13. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  14. package/dist/server/cli/headless/tool-watchdog.js +8 -0
  15. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  16. package/dist/server/cli/improvisation-session-manager.d.ts +8 -1
  17. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  18. package/dist/server/cli/improvisation-session-manager.js +45 -3
  19. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  20. package/dist/server/index.js +0 -4
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/mcp/bouncer-integration.d.ts +2 -0
  23. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  24. package/dist/server/mcp/bouncer-integration.js +55 -39
  25. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  26. package/dist/server/mcp/bouncer-sandbox.d.ts +60 -0
  27. package/dist/server/mcp/bouncer-sandbox.d.ts.map +1 -0
  28. package/dist/server/mcp/bouncer-sandbox.js +182 -0
  29. package/dist/server/mcp/bouncer-sandbox.js.map +1 -0
  30. package/dist/server/mcp/security-patterns.d.ts +6 -12
  31. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  32. package/dist/server/mcp/security-patterns.js +197 -10
  33. package/dist/server/mcp/security-patterns.js.map +1 -1
  34. package/dist/server/services/platform.d.ts +6 -4
  35. package/dist/server/services/platform.d.ts.map +1 -1
  36. package/dist/server/services/platform.js +30 -11
  37. package/dist/server/services/platform.js.map +1 -1
  38. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  39. package/dist/server/services/terminal/pty-manager.js +3 -1
  40. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  41. package/dist/server/services/websocket/handler.d.ts +0 -1
  42. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  43. package/dist/server/services/websocket/handler.js +7 -2
  44. package/dist/server/services/websocket/handler.js.map +1 -1
  45. package/dist/server/services/websocket/quality-handlers.d.ts +4 -0
  46. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -0
  47. package/dist/server/services/websocket/quality-handlers.js +106 -0
  48. package/dist/server/services/websocket/quality-handlers.js.map +1 -0
  49. package/dist/server/services/websocket/quality-service.d.ts +54 -0
  50. package/dist/server/services/websocket/quality-service.d.ts.map +1 -0
  51. package/dist/server/services/websocket/quality-service.js +766 -0
  52. package/dist/server/services/websocket/quality-service.js.map +1 -0
  53. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  54. package/dist/server/services/websocket/session-handlers.js +23 -0
  55. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  56. package/dist/server/services/websocket/types.d.ts +2 -2
  57. package/dist/server/services/websocket/types.d.ts.map +1 -1
  58. package/package.json +2 -1
  59. package/server/cli/headless/claude-invoker.ts +7 -5
  60. package/server/cli/headless/runner.ts +17 -4
  61. package/server/cli/headless/stall-assessor.ts +4 -1
  62. package/server/cli/headless/tool-watchdog.ts +8 -0
  63. package/server/cli/improvisation-session-manager.ts +50 -3
  64. package/server/index.ts +0 -4
  65. package/server/mcp/bouncer-integration.ts +66 -44
  66. package/server/mcp/bouncer-sandbox.ts +214 -0
  67. package/server/mcp/security-patterns.ts +206 -10
  68. package/server/services/platform.ts +29 -11
  69. package/server/services/terminal/pty-manager.ts +3 -1
  70. package/server/services/websocket/handler.ts +7 -2
  71. package/server/services/websocket/quality-handlers.ts +140 -0
  72. package/server/services/websocket/quality-service.ts +922 -0
  73. package/server/services/websocket/session-handlers.ts +26 -0
  74. 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
- if (this.ws && this.isConnected) {
193
- try {
194
- this.ws.send(JSON.stringify({ type: 'ping' }))
195
- } catch {
196
- // Ignore send errors - will reconnect if disconnected
197
- }
198
- }
199
- }, 2 * 60 * 1000)
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
- // Heartbeat response, ignore
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
- const OUTPUT_COALESCE_MS = platform() === 'darwin' ? 24 : 8;
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
+ }