mstro-app 0.3.0 → 0.3.4

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 (121) hide show
  1. package/README.md +3 -19
  2. package/bin/mstro.js +62 -174
  3. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  4. package/dist/server/cli/headless/claude-invoker.js +4 -3
  5. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  6. package/dist/server/cli/headless/mcp-config.js +2 -2
  7. package/dist/server/cli/headless/mcp-config.js.map +1 -1
  8. package/dist/server/cli/headless/runner.d.ts +6 -1
  9. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  10. package/dist/server/cli/headless/runner.js +36 -4
  11. package/dist/server/cli/headless/runner.js.map +1 -1
  12. package/dist/server/cli/headless/types.d.ts +1 -1
  13. package/dist/server/cli/headless/types.d.ts.map +1 -1
  14. package/dist/server/cli/improvisation-session-manager.d.ts +2 -2
  15. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  16. package/dist/server/cli/improvisation-session-manager.js +3 -2
  17. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  18. package/dist/server/index.js +6 -1
  19. package/dist/server/index.js.map +1 -1
  20. package/dist/server/mcp/bouncer-integration.d.ts +1 -1
  21. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  22. package/dist/server/mcp/bouncer-integration.js +85 -114
  23. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  24. package/dist/server/mcp/security-audit.d.ts +3 -3
  25. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  26. package/dist/server/mcp/security-audit.js.map +1 -1
  27. package/dist/server/mcp/server.js +3 -2
  28. package/dist/server/mcp/server.js.map +1 -1
  29. package/dist/server/services/analytics.d.ts +2 -2
  30. package/dist/server/services/analytics.d.ts.map +1 -1
  31. package/dist/server/services/analytics.js.map +1 -1
  32. package/dist/server/services/files.js +7 -7
  33. package/dist/server/services/files.js.map +1 -1
  34. package/dist/server/services/pathUtils.js +1 -1
  35. package/dist/server/services/pathUtils.js.map +1 -1
  36. package/dist/server/services/platform.d.ts +2 -2
  37. package/dist/server/services/platform.d.ts.map +1 -1
  38. package/dist/server/services/platform.js.map +1 -1
  39. package/dist/server/services/sentry.d.ts +1 -1
  40. package/dist/server/services/sentry.d.ts.map +1 -1
  41. package/dist/server/services/sentry.js.map +1 -1
  42. package/dist/server/services/terminal/pty-manager.d.ts +10 -0
  43. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  44. package/dist/server/services/terminal/pty-manager.js +32 -4
  45. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  46. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  47. package/dist/server/services/websocket/file-utils.d.ts +4 -0
  48. package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
  49. package/dist/server/services/websocket/file-utils.js +48 -23
  50. package/dist/server/services/websocket/file-utils.js.map +1 -1
  51. package/dist/server/services/websocket/git-handlers.js +17 -17
  52. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  53. package/dist/server/services/websocket/git-pr-handlers.js +3 -3
  54. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  55. package/dist/server/services/websocket/git-worktree-handlers.js +10 -10
  56. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  57. package/dist/server/services/websocket/handler.js +1 -1
  58. package/dist/server/services/websocket/handler.js.map +1 -1
  59. package/dist/server/services/websocket/session-handlers.d.ts +1 -1
  60. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  61. package/dist/server/services/websocket/session-handlers.js +12 -11
  62. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  63. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  64. package/dist/server/services/websocket/terminal-handlers.js +1 -1
  65. package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
  66. package/dist/server/services/websocket/types.d.ts.map +1 -1
  67. package/dist/server/utils/agent-manager.d.ts +22 -2
  68. package/dist/server/utils/agent-manager.d.ts.map +1 -1
  69. package/dist/server/utils/agent-manager.js +2 -2
  70. package/dist/server/utils/agent-manager.js.map +1 -1
  71. package/dist/server/utils/paths.d.ts +0 -12
  72. package/dist/server/utils/paths.d.ts.map +1 -1
  73. package/dist/server/utils/paths.js +0 -12
  74. package/dist/server/utils/paths.js.map +1 -1
  75. package/dist/server/utils/port-manager.js.map +1 -1
  76. package/package.json +4 -3
  77. package/server/README.md +0 -1
  78. package/server/cli/headless/claude-invoker.ts +21 -16
  79. package/server/cli/headless/mcp-config.ts +8 -8
  80. package/server/cli/headless/runner.ts +32 -4
  81. package/server/cli/headless/types.ts +1 -1
  82. package/server/cli/improvisation-session-manager.ts +8 -7
  83. package/server/index.ts +15 -9
  84. package/server/mcp/README.md +0 -5
  85. package/server/mcp/bouncer-integration.ts +116 -188
  86. package/server/mcp/security-audit.ts +4 -4
  87. package/server/mcp/server.ts +6 -5
  88. package/server/services/analytics.ts +3 -3
  89. package/server/services/files.ts +13 -13
  90. package/server/services/pathUtils.ts +2 -2
  91. package/server/services/platform.ts +5 -5
  92. package/server/services/sentry.ts +1 -1
  93. package/server/services/terminal/pty-manager.ts +36 -9
  94. package/server/services/websocket/file-explorer-handlers.ts +1 -1
  95. package/server/services/websocket/file-utils.ts +52 -28
  96. package/server/services/websocket/git-handlers.ts +34 -34
  97. package/server/services/websocket/git-pr-handlers.ts +6 -6
  98. package/server/services/websocket/git-worktree-handlers.ts +20 -20
  99. package/server/services/websocket/handler.ts +2 -2
  100. package/server/services/websocket/session-handlers.ts +31 -30
  101. package/server/services/websocket/tab-handlers.ts +1 -1
  102. package/server/services/websocket/terminal-handlers.ts +2 -2
  103. package/server/services/websocket/types.ts +2 -0
  104. package/server/utils/agent-manager.ts +6 -6
  105. package/server/utils/paths.ts +0 -14
  106. package/server/utils/port-manager.ts +1 -1
  107. package/bin/configure-claude.js +0 -298
  108. package/dist/server/mcp/bouncer-cli.d.ts +0 -3
  109. package/dist/server/mcp/bouncer-cli.d.ts.map +0 -1
  110. package/dist/server/mcp/bouncer-cli.js +0 -99
  111. package/dist/server/mcp/bouncer-cli.js.map +0 -1
  112. package/hooks/bouncer.sh +0 -145
  113. package/server/cli/headless/output-utils.test.ts +0 -225
  114. package/server/cli/headless/stall-assessor.test.ts +0 -165
  115. package/server/cli/headless/tool-watchdog.test.ts +0 -429
  116. package/server/mcp/bouncer-cli.ts +0 -127
  117. package/server/mcp/bouncer-integration.test.ts +0 -161
  118. package/server/mcp/security-patterns.test.ts +0 -258
  119. package/server/services/platform.test.ts +0 -1304
  120. package/server/services/websocket/autocomplete.test.ts +0 -194
  121. package/server/services/websocket/handler.test.ts +0 -20
@@ -21,6 +21,10 @@ import type {
21
21
  ToolUseEvent,
22
22
  } from './types.js';
23
23
 
24
+ /** Parsed JSON from Claude CLI stream — structure varies by event type */
25
+ // biome-ignore lint/suspicious/noExplicitAny: external CLI stream JSON with heterogeneous shapes
26
+ type StreamJson = any;
27
+
24
28
  export interface ClaudeInvokerOptions {
25
29
  config: ResolvedHeadlessConfig;
26
30
  runningProcesses: Map<number, ChildProcess>;
@@ -281,7 +285,7 @@ interface StreamHandlerContext {
281
285
  }
282
286
 
283
287
  function handleSessionCapture(
284
- parsed: any,
288
+ parsed: StreamJson,
285
289
  captured: { claudeSessionId?: string }
286
290
  ): void {
287
291
  if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.session_id) {
@@ -292,7 +296,7 @@ function handleSessionCapture(
292
296
  }
293
297
  }
294
298
 
295
- function handleThinkingDelta(event: any, ctx: StreamHandlerContext): string {
299
+ function handleThinkingDelta(event: StreamJson, ctx: StreamHandlerContext): string {
296
300
  if (
297
301
  event.type !== 'content_block_delta' ||
298
302
  event.delta?.type !== 'thinking_delta' ||
@@ -324,7 +328,7 @@ function handleThinkingDelta(event: any, ctx: StreamHandlerContext): string {
324
328
  return updated;
325
329
  }
326
330
 
327
- function handleTextDelta(event: any, ctx: StreamHandlerContext): string {
331
+ function handleTextDelta(event: StreamJson, ctx: StreamHandlerContext): string {
328
332
  if (
329
333
  event.type !== 'content_block_delta' ||
330
334
  event.delta?.type !== 'text_delta' ||
@@ -366,7 +370,7 @@ function handleTextDelta(event: any, ctx: StreamHandlerContext): string {
366
370
  return updated;
367
371
  }
368
372
 
369
- function handleToolStart(event: any, ctx: StreamHandlerContext): void {
373
+ function handleToolStart(event: StreamJson, ctx: StreamHandlerContext): void {
370
374
  if (
371
375
  event.type !== 'content_block_start' ||
372
376
  event.content_block?.type !== 'tool_use'
@@ -399,7 +403,7 @@ function handleToolStart(event: any, ctx: StreamHandlerContext): void {
399
403
  }
400
404
  }
401
405
 
402
- function handleToolInputDelta(event: any, ctx: StreamHandlerContext): void {
406
+ function handleToolInputDelta(event: StreamJson, ctx: StreamHandlerContext): void {
403
407
  if (
404
408
  event.type !== 'content_block_delta' ||
405
409
  event.delta?.type !== 'input_json_delta'
@@ -420,7 +424,7 @@ function handleToolInputDelta(event: any, ctx: StreamHandlerContext): void {
420
424
  }
421
425
  }
422
426
 
423
- function handleToolComplete(event: any, ctx: StreamHandlerContext): void {
427
+ function handleToolComplete(event: StreamJson, ctx: StreamHandlerContext): void {
424
428
  if (event.type !== 'content_block_stop') {
425
429
  return;
426
430
  }
@@ -431,7 +435,7 @@ function handleToolComplete(event: any, ctx: StreamHandlerContext): void {
431
435
  return;
432
436
  }
433
437
 
434
- let completeInput: any = {};
438
+ let completeInput: Record<string, unknown> = {};
435
439
  try {
436
440
  completeInput = JSON.parse(toolBuffer.inputJson);
437
441
  } catch (_e) {
@@ -460,7 +464,7 @@ function handleToolComplete(event: any, ctx: StreamHandlerContext): void {
460
464
  }
461
465
 
462
466
  /** Accumulate input tokens from a message_start event. Returns true if any tokens were added. */
463
- function handleMessageStartTokens(event: any, ctx: StreamHandlerContext): boolean {
467
+ function handleMessageStartTokens(event: StreamJson, ctx: StreamHandlerContext): boolean {
464
468
  if (event.type !== 'message_start' || !event.message?.usage) return false;
465
469
  const usage = event.message.usage;
466
470
  ctx.currentStepOutputTokens = 0;
@@ -488,7 +492,7 @@ function handleMessageStartTokens(event: any, ctx: StreamHandlerContext): boolea
488
492
  * message_delta event are cumulative" and there can be "one or more message_delta
489
493
  * events" per message. We track the delta from the previous value to avoid
490
494
  * double-counting when multiple message_delta events fire per step. */
491
- function handleMessageDeltaTokens(event: any, ctx: StreamHandlerContext): boolean {
495
+ function handleMessageDeltaTokens(event: StreamJson, ctx: StreamHandlerContext): boolean {
492
496
  if (event.type !== 'message_delta' || !event.usage) return false;
493
497
  if (typeof event.usage.output_tokens !== 'number') return false;
494
498
  const increment = event.usage.output_tokens - ctx.currentStepOutputTokens;
@@ -500,7 +504,7 @@ function handleMessageDeltaTokens(event: any, ctx: StreamHandlerContext): boolea
500
504
  return true;
501
505
  }
502
506
 
503
- function handleTokenUsage(event: any, ctx: StreamHandlerContext): void {
507
+ function handleTokenUsage(event: StreamJson, ctx: StreamHandlerContext): void {
504
508
  const changed = handleMessageStartTokens(event, ctx) || handleMessageDeltaTokens(event, ctx);
505
509
  if (changed) {
506
510
  ctx.lastTokenActivityTime = Date.now();
@@ -514,7 +518,7 @@ function handleTokenUsage(event: any, ctx: StreamHandlerContext): void {
514
518
  * accumulated stream-based counts which may be incomplete (e.g., when extended thinking
515
519
  * suppresses stream_event emissions).
516
520
  */
517
- function handleResultTokenUsage(parsed: any, ctx: StreamHandlerContext): void {
521
+ function handleResultTokenUsage(parsed: StreamJson, ctx: StreamHandlerContext): void {
518
522
  if (!parsed.usage) return;
519
523
  const u = parsed.usage;
520
524
  const input = (typeof u.input_tokens === 'number' ? u.input_tokens : 0)
@@ -533,7 +537,7 @@ function handleResultTokenUsage(parsed: any, ctx: StreamHandlerContext): void {
533
537
  }
534
538
  }
535
539
 
536
- function handleToolResult(parsed: any, ctx: StreamHandlerContext): void {
540
+ function handleToolResult(parsed: StreamJson, ctx: StreamHandlerContext): void {
537
541
  if (parsed.type !== 'user' || !parsed.message?.content) {
538
542
  return;
539
543
  }
@@ -583,7 +587,7 @@ function processStreamLines(
583
587
  return remainder;
584
588
  }
585
589
 
586
- function processStreamEvent(parsed: any, ctx: StreamHandlerContext): void {
590
+ function processStreamEvent(parsed: StreamJson, ctx: StreamHandlerContext): void {
587
591
  // Handle error events from Claude CLI (API errors, model errors, etc.)
588
592
  if (parsed.type === 'error') {
589
593
  const errorMessage = parsed.error?.message || parsed.message || JSON.stringify(parsed);
@@ -870,12 +874,13 @@ function onToolStart(event: ToolUseEvent, s: ToolTrackingState): void {
870
874
  /** Handle tool_complete events. Extracted to reduce cognitive complexity. */
871
875
  function onToolComplete(event: ToolUseEvent, s: ToolTrackingState): void {
872
876
  const id = event.toolId!;
873
- s.counters.lastToolInputSummary = summarizeToolInput(event.completeInput);
874
- s.toolIdToInput.set(id, event.completeInput);
877
+ const input = event.completeInput ?? {};
878
+ s.counters.lastToolInputSummary = summarizeToolInput(input);
879
+ s.toolIdToInput.set(id, input);
875
880
  if (!s.watchdog) return;
876
881
  const toolName = s.toolIdToName.get(id);
877
882
  if (toolName) {
878
- s.watchdog.startWatch(id, toolName, event.completeInput, () => { s.onTimeout(id); });
883
+ s.watchdog.startWatch(id, toolName, input, () => { s.onTimeout(id); });
879
884
  }
880
885
  }
881
886
 
@@ -12,8 +12,8 @@ import { MCP_SERVER_PATH, MSTRO_ROOT } from '../../utils/paths.js';
12
12
  /**
13
13
  * Load user's MCP servers from ~/.claude.json (global + project-level)
14
14
  */
15
- function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string, any> {
16
- const servers: Record<string, any> = {};
15
+ function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string, unknown> {
16
+ const servers: Record<string, unknown> = {};
17
17
  const claudeConfigPath = join(homedir(), '.claude.json');
18
18
 
19
19
  if (!existsSync(claudeConfigPath)) {
@@ -29,7 +29,7 @@ function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string
29
29
 
30
30
  if (claudeConfig.projects && typeof claudeConfig.projects === 'object') {
31
31
  for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
32
- const projectServers = (projectConfig as any)?.mcpServers;
32
+ const projectServers = (projectConfig as Record<string, unknown>)?.mcpServers;
33
33
  if (workingDir.startsWith(projectPath) && typeof projectServers === 'object') {
34
34
  Object.assign(servers, projectServers);
35
35
  }
@@ -39,8 +39,8 @@ function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string
39
39
  if (verbose) {
40
40
  console.log(`[${new Date().toISOString()}] Loaded ${Object.keys(servers).length} user MCP servers from ~/.claude.json`);
41
41
  }
42
- } catch (parseError: any) {
43
- console.error(`[${new Date().toISOString()}] Failed to parse ~/.claude.json: ${parseError.message}`);
42
+ } catch (parseError: unknown) {
43
+ console.error(`[${new Date().toISOString()}] Failed to parse ~/.claude.json: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
44
44
  }
45
45
 
46
46
  return servers;
@@ -57,7 +57,7 @@ export function generateMcpConfig(workingDir: string, verbose: boolean = false):
57
57
  return null;
58
58
  }
59
59
 
60
- const mcpServers: Record<string, any> = {
60
+ const mcpServers: Record<string, unknown> = {
61
61
  'mstro-bouncer': {
62
62
  command: 'npx',
63
63
  args: ['tsx', MCP_SERVER_PATH],
@@ -80,8 +80,8 @@ export function generateMcpConfig(workingDir: string, verbose: boolean = false):
80
80
  }
81
81
 
82
82
  return configPath;
83
- } catch (error: any) {
84
- console.error(`[${new Date().toISOString()}] Failed to generate MCP config: ${error.message}`);
83
+ } catch (error: unknown) {
84
+ console.error(`[${new Date().toISOString()}] Failed to generate MCP config: ${error instanceof Error ? error.message : String(error)}`);
85
85
  return null;
86
86
  }
87
87
  }
@@ -175,12 +175,40 @@ export class HeadlessRunner {
175
175
  }
176
176
 
177
177
  /**
178
- * Cleanup on exit
178
+ * Cleanup on exit — SIGTERM all tracked processes, then SIGKILL stragglers after 5s
179
179
  */
180
180
  cleanup(): void {
181
- for (const [_pid, process] of this.runningProcesses) {
182
- process.kill();
181
+ if (this.runningProcesses.size === 0) return;
182
+
183
+ const pids = new Set<number>();
184
+ for (const [pid, proc] of this.runningProcesses) {
185
+ pids.add(pid);
186
+ try { proc.kill('SIGTERM'); } catch { /* already dead */ }
187
+ }
188
+
189
+ // SIGKILL fallback after 5 seconds for any process that didn't exit
190
+ setTimeout(() => {
191
+ for (const [pid, proc] of this.runningProcesses) {
192
+ if (pids.has(pid) && !proc.killed) {
193
+ try { proc.kill('SIGKILL'); } catch { /* already dead */ }
194
+ }
195
+ }
196
+ this.runningProcesses.clear();
197
+ }, 5000);
198
+ }
199
+
200
+ /**
201
+ * Sweep for zombie processes — entries in runningProcesses whose underlying
202
+ * process has already exited but whose 'close' event was missed.
203
+ */
204
+ sweepZombies(): number {
205
+ let swept = 0;
206
+ for (const [pid, proc] of this.runningProcesses) {
207
+ if (proc.exitCode !== null || proc.killed) {
208
+ this.runningProcesses.delete(pid);
209
+ swept++;
210
+ }
183
211
  }
184
- this.runningProcesses.clear();
212
+ return swept;
185
213
  }
186
214
  }
@@ -19,7 +19,7 @@ export interface ToolUseEvent {
19
19
  toolId?: string;
20
20
  index?: number;
21
21
  partialJson?: string;
22
- completeInput?: any;
22
+ completeInput?: Record<string, unknown>;
23
23
  result?: string;
24
24
  isError?: boolean;
25
25
  }
@@ -114,7 +114,7 @@ export class ImprovisationSessionManager extends EventEmitter {
114
114
  private currentRunner: HeadlessRunner | null = null;
115
115
  private options: ImprovisationOptions;
116
116
  private pendingApproval?: {
117
- plan: any;
117
+ plan: unknown;
118
118
  resolve: (approved: boolean) => void;
119
119
  };
120
120
  private outputQueue: Array<{ text: string; timestamp: number }> = [];
@@ -129,7 +129,7 @@ export class ImprovisationSessionManager extends EventEmitter {
129
129
  /** Timestamp when current execution started (for accurate elapsed time across reconnects) */
130
130
  private _executionStartTimestamp: number | undefined;
131
131
  /** Buffered events during current execution, for replay on reconnect */
132
- private executionEventLog: Array<{ type: string; data: any; timestamp: number }> = [];
132
+ private executionEventLog: Array<{ type: string; data: unknown; timestamp: number }> = [];
133
133
  /** Set by cancel() to signal the retry loop to exit */
134
134
  private _cancelled: boolean = false;
135
135
 
@@ -383,19 +383,20 @@ export class ImprovisationSessionManager extends EventEmitter {
383
383
  this.emitMovementComplete(movement, result, _execStart, sequenceNumber);
384
384
  return movement;
385
385
 
386
- } catch (error: any) {
386
+ } catch (error: unknown) {
387
387
  this._isExecuting = false;
388
388
  this._executionStartTimestamp = undefined;
389
389
  this.executionEventLog = [];
390
390
  this.currentRunner = null;
391
391
  this.emit('onMovementError', error);
392
+ const errorMessage = error instanceof Error ? error.message : String(error);
392
393
  trackEvent(AnalyticsEvents.IMPROVISE_MOVEMENT_ERROR, {
393
- error_message: error.message?.slice(0, 200),
394
+ error_message: errorMessage.slice(0, 200),
394
395
  sequence_number: this.history.movements.length + 1,
395
396
  duration_ms: Date.now() - _execStart,
396
397
  model: this.options.model || 'default',
397
398
  });
398
- this.queueOutput(`\n❌ Error: ${error.message}\n`);
399
+ this.queueOutput(`\n❌ Error: ${errorMessage}\n`);
399
400
  this.flushOutputQueue();
400
401
  throw error;
401
402
  } finally {
@@ -1510,7 +1511,7 @@ export class ImprovisationSessionManager extends EventEmitter {
1510
1511
  * Request user approval for a plan
1511
1512
  * Returns a promise that resolves when the user approves/rejects
1512
1513
  */
1513
- async requestApproval(plan: any): Promise<boolean> {
1514
+ async requestApproval(plan: unknown): Promise<boolean> {
1514
1515
  return new Promise((resolve) => {
1515
1516
  this.pendingApproval = { plan, resolve };
1516
1517
  this.emit('onApprovalRequired', plan);
@@ -1559,7 +1560,7 @@ export class ImprovisationSessionManager extends EventEmitter {
1559
1560
  * Get buffered execution events for replay on reconnect.
1560
1561
  * Only meaningful while isExecuting is true.
1561
1562
  */
1562
- getExecutionEventLog(): Array<{ type: string; data: any; timestamp: number }> {
1563
+ getExecutionEventLog(): Array<{ type: string; data: unknown; timestamp: number }> {
1563
1564
  return this.executionEventLog;
1564
1565
  }
1565
1566
 
package/server/index.ts CHANGED
@@ -7,11 +7,11 @@
7
7
 
8
8
  import { randomBytes } from 'node:crypto'
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
10
- import type { IncomingMessage } from 'node:http'
10
+ import type { IncomingMessage, Server } from 'node:http'
11
11
  import { homedir } from 'node:os'
12
12
  import { basename, join } from 'node:path'
13
13
  import { serve } from '@hono/node-server'
14
- import { Hono } from 'hono'
14
+ import { type Context, Hono, type Next } from 'hono'
15
15
  import { cors } from 'hono/cors'
16
16
  import { logger } from 'hono/logger'
17
17
  import { type WebSocket as NodeWebSocket, WebSocketServer } from 'ws'
@@ -26,10 +26,10 @@ import {
26
26
  import { AnalyticsEvents, initAnalytics, shutdownAnalytics, trackEvent } from './services/analytics.js'
27
27
  import { AuthService } from './services/auth.js'
28
28
  import { FileService } from './services/files.js'
29
- import { InstanceRegistry } from './services/instances.js'
29
+ import { InstanceRegistry, type MstroInstance } from './services/instances.js'
30
30
  import { PlatformConnection } from './services/platform.js'
31
31
  import { captureException, flushSentry, initSentry } from './services/sentry.js'
32
- import { getPTYManager } from './services/terminal/pty-manager.js'
32
+ import { getPTYManager, reloadPty } from './services/terminal/pty-manager.js'
33
33
  import { WebSocketImproviseHandler } from './services/websocket/index.js'
34
34
  import type { WSContext } from './services/websocket/types.js'
35
35
  import { findAvailablePort } from './utils/port.js'
@@ -126,7 +126,7 @@ const fileService = new FileService(WORKING_DIR)
126
126
  const wsHandler = new WebSocketImproviseHandler()
127
127
 
128
128
  // Instance registration deferred to startServer() when port is known
129
- let _currentInstance: any
129
+ let _currentInstance: MstroInstance | undefined
130
130
 
131
131
  // Global middleware
132
132
  // In production, restrict CORS to block cross-origin browser requests to localhost.
@@ -149,7 +149,7 @@ app.use('*', logger())
149
149
  // Authentication Middleware
150
150
  // ========================================
151
151
 
152
- const authMiddleware = async (c: any, next: any) => {
152
+ const authMiddleware = async (c: Context, next: Next) => {
153
153
  // Skip auth for health check and config
154
154
  const publicPaths = ['/health', '/api/config']
155
155
  if (publicPaths.some(path => c.req.path.startsWith(path))) {
@@ -207,6 +207,12 @@ app.route('/api/improvise', createImproviseRoutes(WORKING_DIR))
207
207
  app.route('/api/files', createFileRoutes(fileService))
208
208
  app.route('/api/notifications', createNotificationRoutes(WORKING_DIR))
209
209
 
210
+ // Reload node-pty after setup-terminal compiles the native module
211
+ app.post('/api/reload-pty', async (c) => {
212
+ const success = await reloadPty()
213
+ return c.json({ success, available: success })
214
+ })
215
+
210
216
  // ========================================
211
217
  // Static File Serving (Production Only)
212
218
  // ========================================
@@ -257,7 +263,7 @@ function wrapWebSocket(ws: NodeWebSocket, workingDir: string): WSContext {
257
263
  * This allows messages from the web (via platform) to be handled by the same wsHandler
258
264
  */
259
265
  function createPlatformRelayContext(
260
- platformSend: (message: any) => void,
266
+ platformSend: (message: unknown) => void,
261
267
  workingDir: string
262
268
  ): WSContext {
263
269
  return {
@@ -299,7 +305,7 @@ async function startServer() {
299
305
  })
300
306
 
301
307
  // Create WebSocket server attached to the HTTP server
302
- const wss = new WebSocketServer({ server: server as any })
308
+ const wss = new WebSocketServer({ server: server as Server })
303
309
 
304
310
  wss.on('connection', (ws: NodeWebSocket, req: IncomingMessage) => {
305
311
  const url = new URL(req.url || '/', `http://localhost:${PORT}`)
@@ -354,7 +360,7 @@ async function startServer() {
354
360
 
355
361
  // Queue for messages that arrive before relay context is ready
356
362
  // This handles race conditions where initTab arrives before web_connected
357
- let pendingRelayMessages: any[] = []
363
+ let pendingRelayMessages: unknown[] = []
358
364
 
359
365
  // Connect to platform
360
366
  const platformConnection = new PlatformConnection(WORKING_DIR, {
@@ -22,7 +22,6 @@ MCP (Model Context Protocol) server that provides tool approval decisions for Cl
22
22
  - **bouncer-integration.ts** — Core 2-layer security review logic. Orchestrates pattern check → AI analysis flow.
23
23
  - **security-patterns.ts** — Pattern definitions: CRITICAL_THREATS, SAFE_OPERATIONS, NEEDS_AI_REVIEW, SENSITIVE_PATHS.
24
24
  - **security-audit.ts** — Audit logging to `~/.mstro/logs/bouncer-audit.jsonl` (JSON Lines format).
25
- - **bouncer-cli.ts** — Shell-callable wrapper invoked by `~/.claude/hooks/bouncer.sh`. Reads JSON from stdin, outputs decision to stdout.
26
25
 
27
26
  ## Usage
28
27
 
@@ -49,10 +48,6 @@ claude --print \
49
48
 
50
49
  The MCP config is auto-generated by the headless runner at `~/.mstro/mcp-config.json`. It includes the bouncer server plus any user-configured MCP servers from `~/.claude.json`.
51
50
 
52
- ### Hook Integration
53
-
54
- When installed via `mstro configure-hooks`, a shell script at `~/.claude/hooks/bouncer.sh` calls `bouncer-cli.ts` as a PreToolUse hook. This provides security for both mstro sessions and standalone Claude Code usage.
55
-
56
51
  ## Environment Variables
57
52
 
58
53
  | Variable | Default | Description |