mstro-app 0.1.47

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/bin/commands/config.js +145 -0
  4. package/bin/commands/login.js +313 -0
  5. package/bin/commands/logout.js +75 -0
  6. package/bin/commands/status.js +197 -0
  7. package/bin/commands/whoami.js +161 -0
  8. package/bin/configure-claude.js +298 -0
  9. package/bin/mstro.js +581 -0
  10. package/bin/postinstall.js +45 -0
  11. package/bin/release.sh +110 -0
  12. package/dist/server/cli/headless/claude-invoker.d.ts +17 -0
  13. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -0
  14. package/dist/server/cli/headless/claude-invoker.js +311 -0
  15. package/dist/server/cli/headless/claude-invoker.js.map +1 -0
  16. package/dist/server/cli/headless/index.d.ts +13 -0
  17. package/dist/server/cli/headless/index.d.ts.map +1 -0
  18. package/dist/server/cli/headless/index.js +10 -0
  19. package/dist/server/cli/headless/index.js.map +1 -0
  20. package/dist/server/cli/headless/mcp-config.d.ts +11 -0
  21. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -0
  22. package/dist/server/cli/headless/mcp-config.js +76 -0
  23. package/dist/server/cli/headless/mcp-config.js.map +1 -0
  24. package/dist/server/cli/headless/output-utils.d.ts +33 -0
  25. package/dist/server/cli/headless/output-utils.d.ts.map +1 -0
  26. package/dist/server/cli/headless/output-utils.js +101 -0
  27. package/dist/server/cli/headless/output-utils.js.map +1 -0
  28. package/dist/server/cli/headless/prompt-utils.d.ts +21 -0
  29. package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -0
  30. package/dist/server/cli/headless/prompt-utils.js +84 -0
  31. package/dist/server/cli/headless/prompt-utils.js.map +1 -0
  32. package/dist/server/cli/headless/runner.d.ts +24 -0
  33. package/dist/server/cli/headless/runner.d.ts.map +1 -0
  34. package/dist/server/cli/headless/runner.js +99 -0
  35. package/dist/server/cli/headless/runner.js.map +1 -0
  36. package/dist/server/cli/headless/types.d.ts +106 -0
  37. package/dist/server/cli/headless/types.d.ts.map +1 -0
  38. package/dist/server/cli/headless/types.js +4 -0
  39. package/dist/server/cli/headless/types.js.map +1 -0
  40. package/dist/server/cli/improvisation-session-manager.d.ts +155 -0
  41. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -0
  42. package/dist/server/cli/improvisation-session-manager.js +415 -0
  43. package/dist/server/cli/improvisation-session-manager.js.map +1 -0
  44. package/dist/server/index.d.ts +2 -0
  45. package/dist/server/index.d.ts.map +1 -0
  46. package/dist/server/index.js +386 -0
  47. package/dist/server/index.js.map +1 -0
  48. package/dist/server/mcp/bouncer-cli.d.ts +3 -0
  49. package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
  50. package/dist/server/mcp/bouncer-cli.js +99 -0
  51. package/dist/server/mcp/bouncer-cli.js.map +1 -0
  52. package/dist/server/mcp/bouncer-integration.d.ts +36 -0
  53. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -0
  54. package/dist/server/mcp/bouncer-integration.js +301 -0
  55. package/dist/server/mcp/bouncer-integration.js.map +1 -0
  56. package/dist/server/mcp/security-audit.d.ts +52 -0
  57. package/dist/server/mcp/security-audit.d.ts.map +1 -0
  58. package/dist/server/mcp/security-audit.js +118 -0
  59. package/dist/server/mcp/security-audit.js.map +1 -0
  60. package/dist/server/mcp/security-patterns.d.ts +73 -0
  61. package/dist/server/mcp/security-patterns.d.ts.map +1 -0
  62. package/dist/server/mcp/security-patterns.js +247 -0
  63. package/dist/server/mcp/security-patterns.js.map +1 -0
  64. package/dist/server/mcp/server.d.ts +3 -0
  65. package/dist/server/mcp/server.d.ts.map +1 -0
  66. package/dist/server/mcp/server.js +146 -0
  67. package/dist/server/mcp/server.js.map +1 -0
  68. package/dist/server/routes/files.d.ts +9 -0
  69. package/dist/server/routes/files.d.ts.map +1 -0
  70. package/dist/server/routes/files.js +24 -0
  71. package/dist/server/routes/files.js.map +1 -0
  72. package/dist/server/routes/improvise.d.ts +3 -0
  73. package/dist/server/routes/improvise.d.ts.map +1 -0
  74. package/dist/server/routes/improvise.js +72 -0
  75. package/dist/server/routes/improvise.js.map +1 -0
  76. package/dist/server/routes/index.d.ts +10 -0
  77. package/dist/server/routes/index.d.ts.map +1 -0
  78. package/dist/server/routes/index.js +12 -0
  79. package/dist/server/routes/index.js.map +1 -0
  80. package/dist/server/routes/instances.d.ts +10 -0
  81. package/dist/server/routes/instances.d.ts.map +1 -0
  82. package/dist/server/routes/instances.js +47 -0
  83. package/dist/server/routes/instances.js.map +1 -0
  84. package/dist/server/routes/notifications.d.ts +3 -0
  85. package/dist/server/routes/notifications.d.ts.map +1 -0
  86. package/dist/server/routes/notifications.js +136 -0
  87. package/dist/server/routes/notifications.js.map +1 -0
  88. package/dist/server/services/analytics.d.ts +56 -0
  89. package/dist/server/services/analytics.d.ts.map +1 -0
  90. package/dist/server/services/analytics.js +240 -0
  91. package/dist/server/services/analytics.js.map +1 -0
  92. package/dist/server/services/auth.d.ts +26 -0
  93. package/dist/server/services/auth.d.ts.map +1 -0
  94. package/dist/server/services/auth.js +71 -0
  95. package/dist/server/services/auth.js.map +1 -0
  96. package/dist/server/services/client-id.d.ts +10 -0
  97. package/dist/server/services/client-id.d.ts.map +1 -0
  98. package/dist/server/services/client-id.js +61 -0
  99. package/dist/server/services/client-id.js.map +1 -0
  100. package/dist/server/services/credentials.d.ts +39 -0
  101. package/dist/server/services/credentials.d.ts.map +1 -0
  102. package/dist/server/services/credentials.js +110 -0
  103. package/dist/server/services/credentials.js.map +1 -0
  104. package/dist/server/services/files.d.ts +119 -0
  105. package/dist/server/services/files.d.ts.map +1 -0
  106. package/dist/server/services/files.js +560 -0
  107. package/dist/server/services/files.js.map +1 -0
  108. package/dist/server/services/instances.d.ts +52 -0
  109. package/dist/server/services/instances.d.ts.map +1 -0
  110. package/dist/server/services/instances.js +241 -0
  111. package/dist/server/services/instances.js.map +1 -0
  112. package/dist/server/services/pathUtils.d.ts +47 -0
  113. package/dist/server/services/pathUtils.d.ts.map +1 -0
  114. package/dist/server/services/pathUtils.js +124 -0
  115. package/dist/server/services/pathUtils.js.map +1 -0
  116. package/dist/server/services/platform.d.ts +72 -0
  117. package/dist/server/services/platform.d.ts.map +1 -0
  118. package/dist/server/services/platform.js +368 -0
  119. package/dist/server/services/platform.js.map +1 -0
  120. package/dist/server/services/sentry.d.ts +5 -0
  121. package/dist/server/services/sentry.d.ts.map +1 -0
  122. package/dist/server/services/sentry.js +71 -0
  123. package/dist/server/services/sentry.js.map +1 -0
  124. package/dist/server/services/terminal/pty-manager.d.ts +149 -0
  125. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -0
  126. package/dist/server/services/terminal/pty-manager.js +377 -0
  127. package/dist/server/services/terminal/pty-manager.js.map +1 -0
  128. package/dist/server/services/terminal/tmux-manager.d.ts +82 -0
  129. package/dist/server/services/terminal/tmux-manager.d.ts.map +1 -0
  130. package/dist/server/services/terminal/tmux-manager.js +352 -0
  131. package/dist/server/services/terminal/tmux-manager.js.map +1 -0
  132. package/dist/server/services/websocket/autocomplete.d.ts +50 -0
  133. package/dist/server/services/websocket/autocomplete.d.ts.map +1 -0
  134. package/dist/server/services/websocket/autocomplete.js +361 -0
  135. package/dist/server/services/websocket/autocomplete.js.map +1 -0
  136. package/dist/server/services/websocket/file-utils.d.ts +44 -0
  137. package/dist/server/services/websocket/file-utils.d.ts.map +1 -0
  138. package/dist/server/services/websocket/file-utils.js +272 -0
  139. package/dist/server/services/websocket/file-utils.js.map +1 -0
  140. package/dist/server/services/websocket/handler.d.ts +246 -0
  141. package/dist/server/services/websocket/handler.d.ts.map +1 -0
  142. package/dist/server/services/websocket/handler.js +1771 -0
  143. package/dist/server/services/websocket/handler.js.map +1 -0
  144. package/dist/server/services/websocket/index.d.ts +11 -0
  145. package/dist/server/services/websocket/index.d.ts.map +1 -0
  146. package/dist/server/services/websocket/index.js +14 -0
  147. package/dist/server/services/websocket/index.js.map +1 -0
  148. package/dist/server/services/websocket/types.d.ts +214 -0
  149. package/dist/server/services/websocket/types.d.ts.map +1 -0
  150. package/dist/server/services/websocket/types.js +4 -0
  151. package/dist/server/services/websocket/types.js.map +1 -0
  152. package/dist/server/utils/agent-manager.d.ts +69 -0
  153. package/dist/server/utils/agent-manager.d.ts.map +1 -0
  154. package/dist/server/utils/agent-manager.js +269 -0
  155. package/dist/server/utils/agent-manager.js.map +1 -0
  156. package/dist/server/utils/paths.d.ts +25 -0
  157. package/dist/server/utils/paths.d.ts.map +1 -0
  158. package/dist/server/utils/paths.js +38 -0
  159. package/dist/server/utils/paths.js.map +1 -0
  160. package/dist/server/utils/port-manager.d.ts +10 -0
  161. package/dist/server/utils/port-manager.d.ts.map +1 -0
  162. package/dist/server/utils/port-manager.js +60 -0
  163. package/dist/server/utils/port-manager.js.map +1 -0
  164. package/dist/server/utils/port.d.ts +26 -0
  165. package/dist/server/utils/port.d.ts.map +1 -0
  166. package/dist/server/utils/port.js +83 -0
  167. package/dist/server/utils/port.js.map +1 -0
  168. package/hooks/bouncer.sh +138 -0
  169. package/package.json +74 -0
  170. package/server/README.md +191 -0
  171. package/server/cli/headless/claude-invoker.ts +415 -0
  172. package/server/cli/headless/index.ts +39 -0
  173. package/server/cli/headless/mcp-config.ts +87 -0
  174. package/server/cli/headless/output-utils.ts +109 -0
  175. package/server/cli/headless/prompt-utils.ts +108 -0
  176. package/server/cli/headless/runner.ts +133 -0
  177. package/server/cli/headless/types.ts +118 -0
  178. package/server/cli/improvisation-session-manager.ts +531 -0
  179. package/server/index.ts +456 -0
  180. package/server/mcp/README.md +122 -0
  181. package/server/mcp/bouncer-cli.ts +127 -0
  182. package/server/mcp/bouncer-integration.ts +430 -0
  183. package/server/mcp/security-audit.ts +180 -0
  184. package/server/mcp/security-patterns.ts +290 -0
  185. package/server/mcp/server.ts +174 -0
  186. package/server/routes/files.ts +29 -0
  187. package/server/routes/improvise.ts +82 -0
  188. package/server/routes/index.ts +13 -0
  189. package/server/routes/instances.ts +54 -0
  190. package/server/routes/notifications.ts +158 -0
  191. package/server/services/analytics.ts +277 -0
  192. package/server/services/auth.ts +80 -0
  193. package/server/services/client-id.ts +68 -0
  194. package/server/services/credentials.ts +134 -0
  195. package/server/services/files.ts +710 -0
  196. package/server/services/instances.ts +275 -0
  197. package/server/services/pathUtils.ts +158 -0
  198. package/server/services/platform.test.ts +1314 -0
  199. package/server/services/platform.ts +435 -0
  200. package/server/services/sentry.ts +81 -0
  201. package/server/services/terminal/pty-manager.ts +464 -0
  202. package/server/services/terminal/tmux-manager.ts +426 -0
  203. package/server/services/websocket/autocomplete.ts +438 -0
  204. package/server/services/websocket/file-utils.ts +305 -0
  205. package/server/services/websocket/handler.test.ts +20 -0
  206. package/server/services/websocket/handler.ts +2047 -0
  207. package/server/services/websocket/index.ts +40 -0
  208. package/server/services/websocket/types.ts +339 -0
  209. package/server/tsconfig.json +19 -0
  210. package/server/utils/agent-manager.ts +323 -0
  211. package/server/utils/paths.ts +45 -0
  212. package/server/utils/port-manager.ts +70 -0
  213. package/server/utils/port.ts +102 -0
@@ -0,0 +1,305 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * File Utilities
6
+ *
7
+ * File type detection, gitignore parsing, and directory scanning utilities.
8
+ */
9
+
10
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
11
+ import { extname, join, relative, sep } from 'node:path';
12
+ import type { CacheEntry, } from './types.js';
13
+
14
+ // Directories always excluded from autocomplete scanning
15
+ const ALWAYS_IGNORE_DIRS = new Set([
16
+ 'node_modules', 'dist', 'dist-ssr', '.git', '__pycache__',
17
+ '.next', '.turbo', 'build', '.cache', 'coverage',
18
+ '.parcel-cache', '.nuxt', '.output', '.vercel', '.netlify',
19
+ '.nyc_output', 'venv', '.venv'
20
+ ]);
21
+
22
+ // File type categorization for icons
23
+ export const FILE_TYPE_MAP: Record<string, string> = {
24
+ // JavaScript/TypeScript
25
+ '.js': 'javascript',
26
+ '.jsx': 'javascript',
27
+ '.ts': 'typescript',
28
+ '.tsx': 'typescript',
29
+ '.mjs': 'javascript',
30
+ '.cjs': 'javascript',
31
+ // Web
32
+ '.html': 'html',
33
+ '.htm': 'html',
34
+ '.css': 'css',
35
+ '.scss': 'css',
36
+ '.sass': 'css',
37
+ '.less': 'css',
38
+ // Data
39
+ '.json': 'json',
40
+ '.yaml': 'yaml',
41
+ '.yml': 'yaml',
42
+ '.xml': 'xml',
43
+ '.toml': 'config',
44
+ // Config
45
+ '.env': 'config',
46
+ '.gitignore': 'config',
47
+ '.eslintrc': 'config',
48
+ '.prettierrc': 'config',
49
+ // Documentation
50
+ '.md': 'markdown',
51
+ '.mdx': 'markdown',
52
+ '.txt': 'text',
53
+ '.rst': 'text',
54
+ // Images
55
+ '.png': 'image',
56
+ '.jpg': 'image',
57
+ '.jpeg': 'image',
58
+ '.gif': 'image',
59
+ '.svg': 'image',
60
+ '.webp': 'image',
61
+ '.ico': 'image',
62
+ // Other languages
63
+ '.py': 'python',
64
+ '.rb': 'ruby',
65
+ '.go': 'go',
66
+ '.rs': 'rust',
67
+ '.java': 'java',
68
+ '.kt': 'kotlin',
69
+ '.swift': 'swift',
70
+ '.c': 'c',
71
+ '.cpp': 'cpp',
72
+ '.h': 'c',
73
+ '.hpp': 'cpp',
74
+ '.cs': 'csharp',
75
+ '.php': 'php',
76
+ '.sh': 'shell',
77
+ '.bash': 'shell',
78
+ '.zsh': 'shell',
79
+ '.fish': 'shell',
80
+ // Build/Package
81
+ '.lock': 'lock',
82
+ '.log': 'log',
83
+ };
84
+
85
+ // Directory cache
86
+ export const directoryCache = new Map<string, CacheEntry>();
87
+ export const CACHE_TTL_MS = 5000; // 5 second cache
88
+
89
+ /**
90
+ * Get file type from extension
91
+ */
92
+ export function getFileType(filePath: string): string {
93
+ const ext = extname(filePath).toLowerCase();
94
+ return FILE_TYPE_MAP[ext] || 'file';
95
+ }
96
+
97
+ /**
98
+ * Parse .gitignore patterns
99
+ */
100
+ export function parseGitignore(workingDir: string): string[] {
101
+ const gitignorePath = join(workingDir, '.gitignore');
102
+ if (!existsSync(gitignorePath)) {
103
+ return [];
104
+ }
105
+
106
+ try {
107
+ const content = readFileSync(gitignorePath, 'utf-8');
108
+ const patterns = content
109
+ .split('\n')
110
+ .map(line => line.trim())
111
+ .filter(line => line && !line.startsWith('#') && !line.startsWith('!'));
112
+ return patterns;
113
+ } catch (error) {
114
+ console.error('[FileUtils] Error parsing .gitignore:', error);
115
+ return [];
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Check if path matches gitignore pattern
121
+ * Follows gitignore semantics:
122
+ * - Patterns without slashes match anywhere in the path (e.g., "node_modules" matches "foo/node_modules/bar")
123
+ * - Patterns with slashes are anchored to the root (e.g., "src/foo" only matches "src/foo")
124
+ * - Patterns ending with / match directories
125
+ */
126
+ function matchesSinglePattern(normalizedPath: string, pathSegments: string[], pattern: string): boolean {
127
+ const normalizedPattern = pattern.replace(/\\/g, '/');
128
+ const isDirectoryPattern = normalizedPattern.endsWith('/');
129
+ const cleanPattern = isDirectoryPattern ? normalizedPattern.slice(0, -1) : normalizedPattern;
130
+
131
+ if (cleanPattern.includes('/')) {
132
+ return normalizedPath === cleanPattern || normalizedPath.startsWith(`${cleanPattern}/`);
133
+ }
134
+
135
+ if (pathSegments.includes(cleanPattern)) {
136
+ return true;
137
+ }
138
+
139
+ if (cleanPattern.includes('*')) {
140
+ const regexPattern = cleanPattern
141
+ .replace(/\./g, '\\.')
142
+ .replace(/\*\*/g, '.*')
143
+ .replace(/\*/g, '[^/]*');
144
+ return new RegExp(`(^|/)${regexPattern}($|/)`).test(normalizedPath);
145
+ }
146
+
147
+ return false;
148
+ }
149
+
150
+ export function isIgnored(path: string, patterns: string[]): boolean {
151
+ const normalizedPath = path.replace(/\\/g, '/');
152
+ const pathSegments = normalizedPath.split('/');
153
+ return patterns.some(pattern => matchesSinglePattern(normalizedPath, pathSegments, pattern));
154
+ }
155
+
156
+ /**
157
+ * Recursively scan directory with depth limit for performance
158
+ */
159
+ function shouldScanEntry(entry: { name: string; isDirectory: () => boolean }, relativePath: string, gitignorePatterns: string[]): boolean {
160
+ if (entry.name.startsWith('.')) return false;
161
+ if (entry.isDirectory() && ALWAYS_IGNORE_DIRS.has(entry.name)) return false;
162
+ if (gitignorePatterns.length > 0 && isIgnored(relativePath, gitignorePatterns)) return false;
163
+ return true;
164
+ }
165
+
166
+ export function scanDirectoryRecursiveWithDepth(
167
+ dir: string,
168
+ baseDir: string,
169
+ gitignorePatterns: string[],
170
+ results: Array<{ relativePath: string; isDirectory: boolean }> = [],
171
+ maxResults: number = 1000,
172
+ maxDepth: number = 10,
173
+ currentDepth: number = 0
174
+ ): Array<{ relativePath: string; isDirectory: boolean }> {
175
+ try {
176
+ if (results.length >= maxResults || currentDepth >= maxDepth) {
177
+ return results;
178
+ }
179
+
180
+ const entries = readdirSync(dir, { withFileTypes: true });
181
+
182
+ for (const entry of entries) {
183
+ if (results.length >= maxResults) break;
184
+
185
+ const fullPath = join(dir, entry.name);
186
+ const relativePath = relative(baseDir, fullPath);
187
+
188
+ if (!shouldScanEntry(entry, relativePath, gitignorePatterns)) continue;
189
+
190
+ results.push({ relativePath, isDirectory: entry.isDirectory() });
191
+
192
+ if (entry.isDirectory() && results.length < maxResults && currentDepth + 1 < maxDepth) {
193
+ scanDirectoryRecursiveWithDepth(fullPath, baseDir, gitignorePatterns, results, maxResults, maxDepth, currentDepth + 1);
194
+ }
195
+ }
196
+ } catch {
197
+ // Skip directories we can't read
198
+ }
199
+
200
+ return results;
201
+ }
202
+
203
+ /**
204
+ * Check if a path is in a safe location for reading (outside working dir)
205
+ */
206
+ export function isPathInSafeLocation(fullPath: string): boolean {
207
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
208
+ const safePatterns = [
209
+ join(homeDir, 'Desktop'),
210
+ join(homeDir, 'Downloads'),
211
+ join(homeDir, 'Pictures'),
212
+ join(homeDir, 'Documents'),
213
+ '/var/folders/', // macOS temp directories for screenshots
214
+ '/tmp/',
215
+ process.env.TMPDIR || '/tmp'
216
+ ];
217
+ return safePatterns.some(safePath => fullPath.startsWith(safePath));
218
+ }
219
+
220
+ /**
221
+ * Check if a file is an image based on extension
222
+ */
223
+ export function isImageFile(filePath: string): boolean {
224
+ const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff', '.ico', '.svg'];
225
+ const ext = filePath.toLowerCase().split('.').pop();
226
+ return ext ? imageExtensions.includes(`.${ext}`) : false;
227
+ }
228
+
229
+ type FileContentResult = { path: string; fileName: string; content: string; size?: number; modifiedAt?: string; isImage?: boolean; mimeType?: string; error?: string };
230
+
231
+ function readDirectoryContent(fullPath: string, filePath: string, fileName: string): FileContentResult {
232
+ try {
233
+ const entries = readdirSync(fullPath, { withFileTypes: true });
234
+ const listing = entries
235
+ .filter(e => !e.name.startsWith('.'))
236
+ .sort((a, b) => {
237
+ if (a.isDirectory() && !b.isDirectory()) return -1;
238
+ if (!a.isDirectory() && b.isDirectory()) return 1;
239
+ return a.name.localeCompare(b.name);
240
+ })
241
+ .map(e => e.isDirectory() ? `${e.name}/` : e.name)
242
+ .join('\n');
243
+ return { path: filePath, fileName, content: listing, size: listing.length };
244
+ } catch {
245
+ return { path: filePath, fileName, content: '', error: 'Cannot read directory' };
246
+ }
247
+ }
248
+
249
+ function readImageContent(fullPath: string, filePath: string, fileName: string, stats: { size: number; mtime: Date }): FileContentResult {
250
+ const buffer = readFileSync(fullPath);
251
+ const ext = fullPath.toLowerCase().split('.').pop() || 'png';
252
+ const mimeType = ext === 'svg' ? 'image/svg+xml' : ext === 'jpg' ? 'image/jpeg' : `image/${ext}`;
253
+ return {
254
+ path: filePath, fileName,
255
+ content: buffer.toString('base64'),
256
+ size: stats.size, modifiedAt: stats.mtime.toISOString(),
257
+ isImage: true, mimeType
258
+ };
259
+ }
260
+
261
+ function readTextContent(fullPath: string, filePath: string, fileName: string, stats: { size: number; mtime: Date }): FileContentResult {
262
+ return {
263
+ path: filePath, fileName,
264
+ content: readFileSync(fullPath, 'utf-8'),
265
+ size: stats.size, modifiedAt: stats.mtime.toISOString()
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Read file content for context injection
271
+ */
272
+ export function readFileContent(filePath: string, workingDir: string): FileContentResult {
273
+ try {
274
+ const fullPath = filePath.startsWith('/') ? filePath : join(workingDir, filePath);
275
+ const fileName = fullPath.split(sep).pop() || filePath;
276
+
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
+ }
297
+
298
+ return isImage
299
+ ? readImageContent(fullPath, filePath, fileName, stats)
300
+ : readTextContent(fullPath, filePath, fileName, stats);
301
+ } catch (error: any) {
302
+ 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' };
304
+ }
305
+ }
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ describe('WebSocket handler code quality', () => {
6
+ const handlerSource = readFileSync(
7
+ join(import.meta.dirname || __dirname, 'handler.ts'),
8
+ 'utf-8'
9
+ )
10
+
11
+ it('does not use require() — ESM only', () => {
12
+ // Ensure no require() calls exist (the bug was require('fs').mkdirSync)
13
+ const requireCalls = handlerSource.match(/require\s*\(/g)
14
+ expect(requireCalls).toBeNull()
15
+ })
16
+
17
+ it('imports mkdirSync from fs at the top level', () => {
18
+ expect(handlerSource).toContain("import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'")
19
+ })
20
+ })