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
@@ -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: any) {
30
- _ptyLoadError = error.message || 'Failed to load node-pty';
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
  */
@@ -240,7 +267,7 @@ export class PTYManager extends EventEmitter {
240
267
  // wraps echoed chars in multi-part ANSI sequences (RPROMPT, syntax highlighting).
241
268
  // A longer window on macOS ensures these multi-part sequences arrive as one chunk,
242
269
  // which the browser's predictive echo can match correctly.
243
- const OUTPUT_COALESCE_MS = platform() === 'darwin' ? 16 : 8;
270
+ const OUTPUT_COALESCE_MS = platform() === 'darwin' ? 24 : 8;
244
271
  // High-water mark: flush immediately when buffer exceeds this size
245
272
  // to prevent unbounded memory growth during high-output commands (e.g. `yes`)
246
273
  const OUTPUT_HIGH_WATER = 64 * 1024; // 64KB
@@ -284,9 +311,9 @@ export class PTYManager extends EventEmitter {
284
311
  });
285
312
 
286
313
  return { shell: session.shell, cwd, isReconnect: false };
287
- } catch (error: any) {
314
+ } catch (error: unknown) {
288
315
  console.error(`[PTYManager] Failed to create terminal ${terminalId}:`, error);
289
- this.emit('error', terminalId, error.message || 'Failed to create terminal');
316
+ this.emit('error', terminalId, error instanceof Error ? error.message : 'Failed to create terminal');
290
317
  throw error;
291
318
  }
292
319
  }
@@ -304,9 +331,9 @@ export class PTYManager extends EventEmitter {
304
331
  try {
305
332
  session.pty.write(data);
306
333
  return true;
307
- } catch (error: any) {
334
+ } catch (error: unknown) {
308
335
  console.error(`[PTYManager] Error writing to terminal ${terminalId}:`, error);
309
- this.emit('error', terminalId, error.message || 'Write failed');
336
+ this.emit('error', terminalId, error instanceof Error ? error.message : 'Write failed');
310
337
  return false;
311
338
  }
312
339
  }
@@ -324,7 +351,7 @@ export class PTYManager extends EventEmitter {
324
351
  try {
325
352
  session.pty.resize(cols, rows);
326
353
  return true;
327
- } catch (error: any) {
354
+ } catch (error: unknown) {
328
355
  console.error(`[PTYManager] Error resizing terminal ${terminalId}:`, error);
329
356
  return false;
330
357
  }
@@ -353,7 +380,7 @@ export class PTYManager extends EventEmitter {
353
380
  session.pty.kill();
354
381
  this.terminals.delete(terminalId);
355
382
  return true;
356
- } catch (error: any) {
383
+ } catch (error: unknown) {
357
384
  console.error(`[PTYManager] Error closing terminal ${terminalId}:`, error);
358
385
  this.terminals.delete(terminalId);
359
386
  return false;
@@ -44,7 +44,7 @@ function handleReadFile(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessag
44
44
  ctx.send(ws, { type: 'fileContent', tabId, data: readFileContent(msg.data.filePath, workingDir) });
45
45
  }
46
46
 
47
- function sendFileResult(ctx: HandlerContext, ws: WSContext, type: WebSocketResponse['type'], tabId: string, result: any, successData?: Record<string, any>): void {
47
+ function sendFileResult(ctx: HandlerContext, ws: WSContext, type: WebSocketResponse['type'], tabId: string, result: { success: boolean; path?: string; error?: string }, successData?: Record<string, unknown>): void {
48
48
  const data = result.success
49
49
  ? { success: true, path: result.path, ...successData }
50
50
  : { success: false, path: result.path, error: result.error };
@@ -7,7 +7,7 @@
7
7
  * File type detection, gitignore parsing, and directory scanning utilities.
8
8
  */
9
9
 
10
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
10
+ import { existsSync, readdirSync, readFileSync, type Stats, statSync } from 'node:fs';
11
11
  import { extname, join, relative, sep } from 'node:path';
12
12
  import type { CacheEntry, } from './types.js';
13
13
 
@@ -226,6 +226,18 @@ export function isImageFile(filePath: string): boolean {
226
226
  return ext ? imageExtensions.includes(`.${ext}`) : false;
227
227
  }
228
228
 
229
+ /**
230
+ * Check if a file is a binary file that should be base64-encoded (images + PDFs)
231
+ */
232
+ export function isBinaryFile(filePath: string): boolean {
233
+ return isImageFile(filePath) || isPdfFile(filePath);
234
+ }
235
+
236
+ function isPdfFile(filePath: string): boolean {
237
+ const ext = filePath.toLowerCase().split('.').pop();
238
+ return ext === 'pdf';
239
+ }
240
+
229
241
  type FileContentResult = { path: string; fileName: string; content: string; size?: number; modifiedAt?: string; isImage?: boolean; mimeType?: string; error?: string };
230
242
 
231
243
  function readDirectoryContent(fullPath: string, filePath: string, fileName: string): FileContentResult {
@@ -246,10 +258,17 @@ function readDirectoryContent(fullPath: string, filePath: string, fileName: stri
246
258
  }
247
259
  }
248
260
 
249
- function readImageContent(fullPath: string, filePath: string, fileName: string, stats: { size: number; mtime: Date }): FileContentResult {
261
+ function getBinaryMimeType(ext: string): string {
262
+ if (ext === 'svg') return 'image/svg+xml';
263
+ if (ext === 'jpg') return 'image/jpeg';
264
+ if (ext === 'pdf') return 'application/pdf';
265
+ return `image/${ext}`;
266
+ }
267
+
268
+ function readBinaryContent(fullPath: string, filePath: string, fileName: string, stats: { size: number; mtime: Date }): FileContentResult {
250
269
  const buffer = readFileSync(fullPath);
251
270
  const ext = fullPath.toLowerCase().split('.').pop() || 'png';
252
- const mimeType = ext === 'svg' ? 'image/svg+xml' : ext === 'jpg' ? 'image/jpeg' : `image/${ext}`;
271
+ const mimeType = getBinaryMimeType(ext);
253
272
  return {
254
273
  path: filePath, fileName,
255
274
  content: buffer.toString('base64'),
@@ -266,6 +285,31 @@ function readTextContent(fullPath: string, filePath: string, fileName: string, s
266
285
  };
267
286
  }
268
287
 
288
+ function validateFileAccess(fullPath: string, filePath: string, fileName: string, workingDir: string): FileContentResult | null {
289
+ const normalizedPath = join(fullPath);
290
+ if (!normalizedPath.startsWith(join(workingDir)) && !isPathInSafeLocation(normalizedPath)) {
291
+ return { path: filePath, fileName, content: '', error: 'Access denied: path outside allowed locations' };
292
+ }
293
+ if (!existsSync(fullPath)) {
294
+ return { path: filePath, fileName, content: '', error: 'File not found' };
295
+ }
296
+ return null;
297
+ }
298
+
299
+ function readValidatedFile(fullPath: string, filePath: string, fileName: string, stats: Stats): FileContentResult {
300
+ if (stats.isDirectory()) return readDirectoryContent(fullPath, filePath, fileName);
301
+
302
+ const isBin = isBinaryFile(fullPath);
303
+ const MAX_FILE_SIZE = isBin ? 10 * 1024 * 1024 : 1024 * 1024;
304
+ if (stats.size > MAX_FILE_SIZE) {
305
+ return { path: filePath, fileName, content: '', size: stats.size, error: `File too large (${Math.round(stats.size / 1024)}KB). Maximum is ${isBin ? '10MB' : '1MB'}.` };
306
+ }
307
+
308
+ return isBin
309
+ ? readBinaryContent(fullPath, filePath, fileName, stats)
310
+ : readTextContent(fullPath, filePath, fileName, stats);
311
+ }
312
+
269
313
  /**
270
314
  * Read file content for context injection
271
315
  */
@@ -274,32 +318,12 @@ export function readFileContent(filePath: string, workingDir: string): FileConte
274
318
  const fullPath = filePath.startsWith('/') ? filePath : join(workingDir, filePath);
275
319
  const fileName = fullPath.split(sep).pop() || filePath;
276
320
 
277
- const normalizedPath = join(fullPath);
278
- const isInWorkingDir = normalizedPath.startsWith(join(workingDir));
279
- if (!isInWorkingDir && !isPathInSafeLocation(normalizedPath)) {
280
- return { path: filePath, fileName, content: '', error: 'Access denied: path outside allowed locations' };
281
- }
282
-
283
- if (!existsSync(fullPath)) {
284
- return { path: filePath, fileName, content: '', error: 'File not found' };
285
- }
286
-
287
- const stats = statSync(fullPath);
288
- if (stats.isDirectory()) {
289
- return readDirectoryContent(fullPath, filePath, fileName);
290
- }
291
-
292
- const isImage = isImageFile(fullPath);
293
- const MAX_FILE_SIZE = isImage ? 10 * 1024 * 1024 : 1024 * 1024;
294
- if (stats.size > MAX_FILE_SIZE) {
295
- return { path: filePath, fileName, content: '', size: stats.size, error: `File too large (${Math.round(stats.size / 1024)}KB). Maximum is ${isImage ? '10MB' : '1MB'}.` };
296
- }
321
+ const accessError = validateFileAccess(fullPath, filePath, fileName, workingDir);
322
+ if (accessError) return accessError;
297
323
 
298
- return isImage
299
- ? readImageContent(fullPath, filePath, fileName, stats)
300
- : readTextContent(fullPath, filePath, fileName, stats);
301
- } catch (error: any) {
324
+ return readValidatedFile(fullPath, filePath, fileName, statSync(fullPath));
325
+ } catch (error: unknown) {
302
326
  console.error('[FileUtils] Error reading file:', error);
303
- return { path: filePath, fileName: filePath.split(sep).pop() || filePath, content: '', error: error.message || 'Failed to read file' };
327
+ return { path: filePath, fileName: filePath.split(sep).pop() || filePath, content: '', error: (error instanceof Error ? error.message : String(error)) || 'Failed to read file' };
304
328
  }
305
329
  }
@@ -270,8 +270,8 @@ export async function handleGitStatus(ctx: HandlerContext, ws: WSContext, tabId:
270
270
  };
271
271
 
272
272
  ctx.send(ws, { type: 'gitStatus', tabId, data: response });
273
- } catch (error: any) {
274
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
273
+ } catch (error: unknown) {
274
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
275
275
  }
276
276
  }
277
277
 
@@ -293,8 +293,8 @@ async function handleGitStage(ctx: HandlerContext, ws: WSContext, msg: WebSocket
293
293
  }
294
294
 
295
295
  ctx.send(ws, { type: 'gitStaged', tabId, data: { paths: paths || [] } });
296
- } catch (error: any) {
297
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
296
+ } catch (error: unknown) {
297
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
298
298
  }
299
299
  }
300
300
 
@@ -313,8 +313,8 @@ async function handleGitUnstage(ctx: HandlerContext, ws: WSContext, msg: WebSock
313
313
  }
314
314
 
315
315
  ctx.send(ws, { type: 'gitUnstaged', tabId, data: { paths } });
316
- } catch (error: any) {
317
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
316
+ } catch (error: unknown) {
317
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
318
318
  }
319
319
  }
320
320
 
@@ -342,8 +342,8 @@ async function handleGitCommit(ctx: HandlerContext, ws: WSContext, msg: WebSocke
342
342
 
343
343
  ctx.send(ws, { type: 'gitCommitted', tabId, data: { hash, message } });
344
344
  handleGitStatus(ctx, ws, tabId, workingDir);
345
- } catch (error: any) {
346
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
345
+ } catch (error: unknown) {
346
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
347
347
  }
348
348
  }
349
349
 
@@ -463,8 +463,8 @@ Respond with ONLY the commit message, nothing else.`;
463
463
  claude.kill();
464
464
  }, 30000);
465
465
 
466
- } catch (error: any) {
467
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
466
+ } catch (error: unknown) {
467
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
468
468
  }
469
469
  }
470
470
 
@@ -539,8 +539,8 @@ async function handleGitPush(ctx: HandlerContext, ws: WSContext, tabId: string,
539
539
  }
540
540
 
541
541
  ctx.send(ws, { type: 'gitPushed', tabId, data: { output: result.stdout || result.stderr } });
542
- } catch (error: any) {
543
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
542
+ } catch (error: unknown) {
543
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
544
544
  }
545
545
  }
546
546
 
@@ -553,8 +553,8 @@ async function handleGitPull(ctx: HandlerContext, ws: WSContext, tabId: string,
553
553
  }
554
554
 
555
555
  ctx.send(ws, { type: 'gitPulled', tabId, data: { output: result.stdout || result.stderr } });
556
- } catch (error: any) {
557
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
556
+ } catch (error: unknown) {
557
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
558
558
  }
559
559
  }
560
560
 
@@ -580,8 +580,8 @@ async function handleGitLog(ctx: HandlerContext, ws: WSContext, msg: WebSocketMe
580
580
  });
581
581
 
582
582
  ctx.send(ws, { type: 'gitLog', tabId, data: { entries } });
583
- } catch (error: any) {
584
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
583
+ } catch (error: unknown) {
584
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
585
585
  }
586
586
  }
587
587
 
@@ -643,8 +643,8 @@ async function handleGitDiscoverRepos(ctx: HandlerContext, ws: WSContext, tabId:
643
643
  };
644
644
 
645
645
  ctx.send(ws, { type: 'gitReposDiscovered', tabId, data: response });
646
- } catch (error: any) {
647
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
646
+ } catch (error: unknown) {
647
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
648
648
  }
649
649
  }
650
650
 
@@ -712,8 +712,8 @@ async function handleGitListBranches(ctx: HandlerContext, ws: WSContext, tabId:
712
712
  .filter(b => b.name !== 'origin/HEAD');
713
713
 
714
714
  ctx.send(ws, { type: 'gitBranchList', tabId, data: { branches, current: currentBranch } });
715
- } catch (error: any) {
716
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
715
+ } catch (error: unknown) {
716
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
717
717
  }
718
718
  }
719
719
 
@@ -746,8 +746,8 @@ async function handleGitCheckout(ctx: HandlerContext, ws: WSContext, msg: WebSoc
746
746
 
747
747
  ctx.send(ws, { type: 'gitCheckedOut', tabId, data: { branch, previous } });
748
748
  handleGitStatus(ctx, ws, tabId, workingDir);
749
- } catch (error: any) {
750
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
749
+ } catch (error: unknown) {
750
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
751
751
  }
752
752
  }
753
753
 
@@ -773,8 +773,8 @@ async function handleGitCreateBranch(ctx: HandlerContext, ws: WSContext, msg: We
773
773
  }
774
774
 
775
775
  ctx.send(ws, { type: 'gitBranchCreated', tabId, data: { name, hash: hashResult.stdout.trim() } });
776
- } catch (error: any) {
777
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
776
+ } catch (error: unknown) {
777
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
778
778
  }
779
779
  }
780
780
 
@@ -799,8 +799,8 @@ async function handleGitDeleteBranch(ctx: HandlerContext, ws: WSContext, msg: We
799
799
  }
800
800
 
801
801
  ctx.send(ws, { type: 'gitBranchDeleted', tabId, data: { name } });
802
- } catch (error: any) {
803
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
802
+ } catch (error: unknown) {
803
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
804
804
  }
805
805
  }
806
806
 
@@ -833,8 +833,8 @@ async function handleGitDiff(ctx: HandlerContext, ws: WSContext, msg: WebSocketM
833
833
  tabId,
834
834
  data: { path, original, modified, staged: !!staged },
835
835
  });
836
- } catch (error: any) {
837
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
836
+ } catch (error: unknown) {
837
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
838
838
  }
839
839
  }
840
840
 
@@ -863,8 +863,8 @@ async function handleGitListTags(ctx: HandlerContext, ws: WSContext, tabId: stri
863
863
  });
864
864
 
865
865
  ctx.send(ws, { type: 'gitTagList', tabId, data: { tags } });
866
- } catch (error: any) {
867
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
866
+ } catch (error: unknown) {
867
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
868
868
  }
869
869
  }
870
870
 
@@ -893,8 +893,8 @@ async function handleGitCreateTag(ctx: HandlerContext, ws: WSContext, msg: WebSo
893
893
 
894
894
  const hashResult = await executeGitCommand(['rev-parse', '--short', name], workingDir);
895
895
  ctx.send(ws, { type: 'gitTagCreated', tabId, data: { name, hash: hashResult.stdout.trim() } });
896
- } catch (error: any) {
897
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
896
+ } catch (error: unknown) {
897
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
898
898
  }
899
899
  }
900
900
 
@@ -918,7 +918,7 @@ async function handleGitPushTag(ctx: HandlerContext, ws: WSContext, msg: WebSock
918
918
  }
919
919
 
920
920
  ctx.send(ws, { type: 'gitTagPushed', tabId, data: { name: name || 'all', output: result.stderr || result.stdout } });
921
- } catch (error: any) {
922
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
921
+ } catch (error: unknown) {
922
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
923
923
  }
924
924
  }
@@ -49,8 +49,8 @@ async function handleGitGetRemoteInfo(ctx: HandlerContext, ws: WSContext, tabId:
49
49
  preferredBaseBranch,
50
50
  },
51
51
  });
52
- } catch (error: any) {
53
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
52
+ } catch (error: unknown) {
53
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
54
54
  }
55
55
  }
56
56
 
@@ -171,8 +171,8 @@ async function handleGitCreatePR(ctx: HandlerContext, ws: WSContext, msg: WebSoc
171
171
  } else {
172
172
  ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Could not determine remote URL format for PR creation' } });
173
173
  }
174
- } catch (error: any) {
175
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
174
+ } catch (error: unknown) {
175
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
176
176
  }
177
177
  }
178
178
 
@@ -357,7 +357,7 @@ Respond with ONLY the title and description, nothing else.`;
357
357
 
358
358
  setTimeout(() => { claude.kill(); }, 30000);
359
359
 
360
- } catch (error: any) {
361
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
360
+ } catch (error: unknown) {
361
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
362
362
  }
363
363
  }
@@ -55,8 +55,8 @@ async function handleGitWorktreeList(ctx: HandlerContext, ws: WSContext, tabId:
55
55
  }
56
56
  const worktrees = parseWorktreePorcelain(result.stdout);
57
57
  ctx.send(ws, { type: 'gitWorktreeListResult', tabId, data: { worktrees } });
58
- } catch (error: any) {
59
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
58
+ } catch (error: unknown) {
59
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
60
60
  }
61
61
  }
62
62
 
@@ -85,8 +85,8 @@ async function handleGitWorktreeCreate(ctx: HandlerContext, ws: WSContext, msg:
85
85
  tabId,
86
86
  data: { path: wtPath, branch: branchName, head: headResult.stdout.trim() },
87
87
  });
88
- } catch (error: any) {
89
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
88
+ } catch (error: unknown) {
89
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
90
90
  }
91
91
  }
92
92
 
@@ -118,8 +118,8 @@ async function handleGitWorktreeRemove(ctx: HandlerContext, ws: WSContext, msg:
118
118
  await executeGitCommand(['worktree', 'prune'], workingDir);
119
119
 
120
120
  ctx.send(ws, { type: 'gitWorktreeRemoved', tabId, data: { path: wtPath } });
121
- } catch (error: any) {
122
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
121
+ } catch (error: unknown) {
122
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
123
123
  }
124
124
  }
125
125
 
@@ -141,8 +141,8 @@ async function handleTabWorktreeSwitch(ctx: HandlerContext, ws: WSContext, msg:
141
141
 
142
142
  ctx.send(ws, { type: 'tabWorktreeSwitched', tabId: resolvedTabId, data: { tabId: resolvedTabId, worktreePath, branch } });
143
143
  handleGitStatus(ctx, ws, resolvedTabId, worktreePath);
144
- } catch (error: any) {
145
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
144
+ } catch (error: unknown) {
145
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
146
146
  }
147
147
  }
148
148
 
@@ -185,8 +185,8 @@ async function handleGitWorktreePush(ctx: HandlerContext, ws: WSContext, msg: We
185
185
  return;
186
186
  }
187
187
  ctx.send(ws, { type: 'gitWorktreePushed', tabId, data: { output: pushResult.output, upstream: `${pushRemote}/${pushBranch}` } });
188
- } catch (error: any) {
189
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
188
+ } catch (error: unknown) {
189
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
190
190
  }
191
191
  }
192
192
 
@@ -218,8 +218,8 @@ async function handleGitWorktreeCreatePR(ctx: HandlerContext, ws: WSContext, msg
218
218
  const prNumber = prNumberMatch ? parseInt(prNumberMatch[1], 10) : 0;
219
219
 
220
220
  ctx.send(ws, { type: 'gitWorktreePRCreated', tabId, data: { prUrl, prNumber } });
221
- } catch (error: any) {
222
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
221
+ } catch (error: unknown) {
222
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
223
223
  }
224
224
  }
225
225
 
@@ -263,8 +263,8 @@ async function handleGitMergePreview(ctx: HandlerContext, ws: WSContext, msg: We
263
263
  tabId,
264
264
  data: { clean, conflicts, stat, commits, ahead: commits.length },
265
265
  });
266
- } catch (error: any) {
267
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
266
+ } catch (error: unknown) {
267
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
268
268
  }
269
269
  }
270
270
 
@@ -362,8 +362,8 @@ async function handleGitWorktreeMerge(ctx: HandlerContext, ws: WSContext, msg: W
362
362
  await cleanupAfterMerge(mainPath, sourceBranch, strategy, !!deleteWorktree, !!deleteBranch);
363
363
 
364
364
  ctx.send(ws, { type: 'gitWorktreeMergeResult', tabId, data: { success: true, mergeCommit: commitHashResult.stdout.trim() } });
365
- } catch (error: any) {
366
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
365
+ } catch (error: unknown) {
366
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
367
367
  }
368
368
  }
369
369
 
@@ -378,8 +378,8 @@ async function handleGitMergeAbort(ctx: HandlerContext, ws: WSContext, tabId: st
378
378
  }
379
379
 
380
380
  ctx.send(ws, { type: 'gitMergeAborted', tabId, data: { aborted: true } });
381
- } catch (error: any) {
382
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
381
+ } catch (error: unknown) {
382
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
383
383
  }
384
384
  }
385
385
 
@@ -397,7 +397,7 @@ async function handleGitMergeComplete(ctx: HandlerContext, ws: WSContext, _msg:
397
397
  const mergeCommit = hashResult.stdout.trim();
398
398
 
399
399
  ctx.send(ws, { type: 'gitMergeCompleted', tabId, data: { success: true, mergeCommit } });
400
- } catch (error: any) {
401
- ctx.send(ws, { type: 'gitError', tabId, data: { error: error.message } });
400
+ } catch (error: unknown) {
401
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
402
402
  }
403
403
  }
@@ -103,12 +103,12 @@ export class WebSocketImproviseHandler implements HandlerContext {
103
103
  delete msg._permission;
104
104
 
105
105
  await this.dispatchMessage(ws, msg, tabId, workingDir, permission);
106
- } catch (error: any) {
106
+ } catch (error: unknown) {
107
107
  console.error('[WebSocketImproviseHandler] Error handling message:', error);
108
108
  captureException(error, { context: 'websocket.handleMessage' });
109
109
  this.send(ws, {
110
110
  type: 'error',
111
- data: { message: error.message }
111
+ data: { message: error instanceof Error ? error.message : String(error) }
112
112
  });
113
113
  }
114
114
  }