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,438 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Autocomplete Service
6
+ *
7
+ * File autocomplete with frecency scoring and fuzzy matching.
8
+ */
9
+
10
+ import { existsSync, readdirSync, statSync } from 'node:fs';
11
+ import { join, } from 'node:path';
12
+ import Fuse, { type FuseResult } from 'fuse.js';
13
+ import {
14
+ CACHE_TTL_MS,
15
+ directoryCache,
16
+ getFileType,
17
+ isIgnored,
18
+ parseGitignore,
19
+ scanDirectoryRecursiveWithDepth
20
+ } from './file-utils.js';
21
+ import type {
22
+ AutocompleteResult,
23
+ FileMetadata,
24
+ FrecencyData,
25
+ } from './types.js';
26
+
27
+ // ========== Scoring Helpers ==========
28
+
29
+ interface ScoredMatch {
30
+ relativePath: string;
31
+ isDirectory: boolean;
32
+ score: number;
33
+ matchedIndices: Array<[number, number]>;
34
+ isRecent: boolean;
35
+ }
36
+
37
+ function compareAutocompleteResults(a: ScoredMatch, b: ScoredMatch): number {
38
+ if (a.isRecent && !b.isRecent) return -1;
39
+ if (!a.isRecent && b.isRecent) return 1;
40
+ if (b.score !== a.score) return b.score - a.score;
41
+ if (a.isDirectory && !b.isDirectory) return -1;
42
+ if (!a.isDirectory && b.isDirectory) return 1;
43
+ return a.relativePath.localeCompare(b.relativePath);
44
+ }
45
+
46
+ function extractFuseMatchIndices(
47
+ result: FuseResult<FileMetadata>
48
+ ): Array<[number, number]> {
49
+ const matchedIndices: Array<[number, number]> = [];
50
+ if (!result.matches) return matchedIndices;
51
+
52
+ for (const match of result.matches) {
53
+ if (!match.indices) continue;
54
+ for (const [start, end] of match.indices) {
55
+ if (match.key === 'fileName') {
56
+ const filenameStart = result.item.relativePath.lastIndexOf('/') + 1;
57
+ matchedIndices.push([filenameStart + start, filenameStart + end + 1]);
58
+ } else {
59
+ matchedIndices.push([start, end + 1]);
60
+ }
61
+ }
62
+ }
63
+
64
+ return matchedIndices;
65
+ }
66
+
67
+ function scoreFileMatch(
68
+ file: FileMetadata,
69
+ baseScore: number,
70
+ query: string,
71
+ frecencyScore: number,
72
+ recentFiles: Set<string>,
73
+ calculateMatchedIndices: (text: string, query: string) => Array<[number, number]>
74
+ ): ScoredMatch {
75
+ const depthPenalty = file.depth * 20;
76
+ const topLevelBonus = file.depth === 1 ? 200 : 0;
77
+ const dirBonus = file.isDirectory ? 100 : 0;
78
+ return {
79
+ relativePath: file.relativePath,
80
+ isDirectory: file.isDirectory,
81
+ score: baseScore + topLevelBonus + frecencyScore - depthPenalty + dirBonus,
82
+ matchedIndices: calculateMatchedIndices(file.relativePath, query),
83
+ isRecent: recentFiles.has(file.relativePath)
84
+ };
85
+ }
86
+
87
+ function shouldIncludeEntry(
88
+ entry: { name: string; isDirectory: () => boolean },
89
+ relativePath: string,
90
+ gitignorePatterns: string[],
91
+ skipDirs: Set<string>
92
+ ): boolean {
93
+ if (entry.name.startsWith('.')) return false;
94
+ if (entry.isDirectory() && skipDirs.has(entry.name)) return false;
95
+ if (gitignorePatterns.length > 0 && isIgnored(relativePath, gitignorePatterns)) return false;
96
+ return true;
97
+ }
98
+
99
+ export class AutocompleteService {
100
+ private frecencyData: FrecencyData = {};
101
+
102
+ constructor(initialFrecencyData: FrecencyData = {}) {
103
+ this.frecencyData = initialFrecencyData;
104
+ }
105
+
106
+ /**
107
+ * Update frecency data
108
+ */
109
+ setFrecencyData(data: FrecencyData): void {
110
+ this.frecencyData = data;
111
+ }
112
+
113
+ /**
114
+ * Get frecency data
115
+ */
116
+ getFrecencyData(): FrecencyData {
117
+ return this.frecencyData;
118
+ }
119
+
120
+ /**
121
+ * Record a file selection for frecency scoring
122
+ */
123
+ recordFileSelection(filePath: string): void {
124
+ const existing = this.frecencyData[filePath];
125
+ if (existing) {
126
+ existing.count++;
127
+ existing.lastUsed = Date.now();
128
+ } else {
129
+ this.frecencyData[filePath] = {
130
+ count: 1,
131
+ lastUsed: Date.now()
132
+ };
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Calculate frecency score for a file
138
+ */
139
+ calculateFrecencyScore(filePath: string): number {
140
+ const entry = this.frecencyData[filePath];
141
+ if (!entry) return 0;
142
+
143
+ const hoursSinceLastUse = (Date.now() - entry.lastUsed) / (1000 * 60 * 60);
144
+ const recencyWeight = Math.max(0, 1 - (hoursSinceLastUse / (24 * 7)));
145
+ const frequencyWeight = Math.log2(entry.count + 1);
146
+
147
+ return frequencyWeight * (0.3 + 0.7 * recencyWeight) * 100;
148
+ }
149
+
150
+ /**
151
+ * Calculate matched character indices for highlighting
152
+ */
153
+ private calculateMatchedIndices(text: string, query: string): Array<[number, number]> {
154
+ if (!query) return [];
155
+
156
+ const indices: Array<[number, number]> = [];
157
+ const textLower = text.toLowerCase();
158
+ const queryLower = query.toLowerCase();
159
+
160
+ let searchStart = 0;
161
+ while (searchStart < textLower.length) {
162
+ const idx = textLower.indexOf(queryLower, searchStart);
163
+ if (idx === -1) break;
164
+ indices.push([idx, idx + queryLower.length]);
165
+ searchStart = idx + 1;
166
+ }
167
+
168
+ return indices;
169
+ }
170
+
171
+ /**
172
+ * Get file completions for autocomplete with directory-scoped navigation
173
+ */
174
+ getFileCompletions(partialPath: string, workingDir: string): AutocompleteResult[] {
175
+ try {
176
+ // Handle @ symbol prefix for file autocomplete
177
+ const isAtSymbol = partialPath.startsWith('@');
178
+ const cleanPath = isAtSymbol ? partialPath.substring(1) : partialPath;
179
+
180
+ // Parse .gitignore patterns
181
+ const gitignorePatterns = parseGitignore(workingDir);
182
+
183
+ // Directory-scoped completion: When path ends with '/', show direct children
184
+ if (cleanPath.endsWith('/')) {
185
+ return this.getDirectoryContentsEnhanced(cleanPath, workingDir, gitignorePatterns);
186
+ }
187
+
188
+ // STRICT PATH SEGMENT MATCHING
189
+ const lastSlashIndex = cleanPath.lastIndexOf('/');
190
+ let scopedDir = workingDir;
191
+ let searchQuery = cleanPath;
192
+ let pathPrefix = '';
193
+ let maxDepth = 10;
194
+
195
+ if (lastSlashIndex !== -1) {
196
+ const dirPath = cleanPath.substring(0, lastSlashIndex);
197
+ const candidateDir = join(workingDir, dirPath);
198
+
199
+ if (existsSync(candidateDir) && statSync(candidateDir).isDirectory()) {
200
+ scopedDir = candidateDir;
201
+ searchQuery = cleanPath.substring(lastSlashIndex + 1);
202
+ pathPrefix = `${dirPath}/`;
203
+ maxDepth = 3;
204
+ }
205
+ } else if (cleanPath === '') {
206
+ maxDepth = 4;
207
+ }
208
+
209
+ const filesWithMetadata = this.getFilesWithCache(scopedDir, gitignorePatterns, maxDepth, pathPrefix);
210
+
211
+ // Track which files are recent
212
+ const recentFiles = new Set<string>();
213
+ for (const file of filesWithMetadata) {
214
+ if (this.calculateFrecencyScore(file.relativePath) > 0) {
215
+ recentFiles.add(file.relativePath);
216
+ }
217
+ }
218
+
219
+ const scoredMatches = searchQuery === ''
220
+ ? this.scoreEmptyQuery(filesWithMetadata)
221
+ : this.scoreWithQuery(filesWithMetadata, searchQuery, recentFiles);
222
+
223
+ const results: AutocompleteResult[] = scoredMatches.slice(0, 15).map(file => {
224
+ const displayPath = file.isDirectory ? `${file.relativePath}/` : file.relativePath;
225
+ return {
226
+ value: displayPath,
227
+ label: displayPath,
228
+ isDirectory: file.isDirectory,
229
+ isRecent: file.isRecent,
230
+ fileType: file.isDirectory ? 'directory' : getFileType(file.relativePath),
231
+ matchedIndices: file.matchedIndices
232
+ };
233
+ });
234
+
235
+ return results;
236
+ } catch (error) {
237
+ console.error('[AutocompleteService] Error getting file completions:', error);
238
+ return [];
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Get files with caching support
244
+ */
245
+ private getFilesWithCache(scopedDir: string, gitignorePatterns: string[], maxDepth: number, pathPrefix: string): FileMetadata[] {
246
+ const patternsHash = gitignorePatterns.length > 0
247
+ ? gitignorePatterns.slice(0, 20).join('|').slice(0, 100)
248
+ : 'none';
249
+ const cacheKey = `${scopedDir}:${maxDepth}:${patternsHash}`;
250
+
251
+ const cached = directoryCache.get(cacheKey);
252
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
253
+ return cached.files.map(f => ({ ...f, relativePath: pathPrefix + f.relativePath }));
254
+ }
255
+
256
+ const allFiles = scanDirectoryRecursiveWithDepth(scopedDir, scopedDir, gitignorePatterns, [], 2000, maxDepth);
257
+ const filesForCache = allFiles.map(file => {
258
+ const fileName = file.relativePath.split('/').pop() || '';
259
+ return {
260
+ relativePath: file.relativePath,
261
+ isDirectory: file.isDirectory,
262
+ fileName,
263
+ depth: file.relativePath.split('/').length
264
+ };
265
+ });
266
+ directoryCache.set(cacheKey, { files: filesForCache, timestamp: Date.now() });
267
+
268
+ return filesForCache.map(f => ({ ...f, relativePath: pathPrefix + f.relativePath }));
269
+ }
270
+
271
+ /**
272
+ * Score files when no search query is provided
273
+ */
274
+ private scoreEmptyQuery(files: FileMetadata[]): ScoredMatch[] {
275
+ return files
276
+ .map(file => {
277
+ const frecencyScore = this.calculateFrecencyScore(file.relativePath);
278
+ const depthPenalty = file.depth * 30;
279
+ const directoryBonus = file.isDirectory ? 50 : 0;
280
+ const topLevelBonus = file.depth === 1 ? 500 : 0;
281
+ return {
282
+ relativePath: file.relativePath,
283
+ isDirectory: file.isDirectory,
284
+ score: frecencyScore + topLevelBonus + directoryBonus - depthPenalty,
285
+ matchedIndices: [] as Array<[number, number]>,
286
+ isRecent: frecencyScore > 0
287
+ };
288
+ })
289
+ .sort(compareAutocompleteResults);
290
+ }
291
+
292
+ /**
293
+ * Score files against a search query using prefix, contains, and fuzzy matching
294
+ */
295
+ private scoreWithQuery(files: FileMetadata[], searchQuery: string, recentFiles: Set<string>): ScoredMatch[] {
296
+ const query = searchQuery.toLowerCase();
297
+ const prefixMatches: FileMetadata[] = [];
298
+ const filenameContainsMatches: FileMetadata[] = [];
299
+ const pathContainsMatches: FileMetadata[] = [];
300
+ const otherFiles: FileMetadata[] = [];
301
+
302
+ for (const file of files) {
303
+ const fileNameLower = file.fileName.toLowerCase();
304
+ const relativePathLower = file.relativePath.toLowerCase();
305
+
306
+ if (fileNameLower.startsWith(query)) {
307
+ prefixMatches.push(file);
308
+ } else if (fileNameLower.includes(query)) {
309
+ filenameContainsMatches.push(file);
310
+ } else if (relativePathLower.includes(query)) {
311
+ pathContainsMatches.push(file);
312
+ } else {
313
+ otherFiles.push(file);
314
+ }
315
+ }
316
+
317
+ const calcIndices = this.calculateMatchedIndices.bind(this);
318
+ const calcFrec = (path: string) => this.calculateFrecencyScore(path);
319
+
320
+ const scored = [
321
+ ...prefixMatches.map(f => {
322
+ const exactBonus = f.fileName.toLowerCase() === query ? 500 : 0;
323
+ return scoreFileMatch(f, 2000 + exactBonus, query, calcFrec(f.relativePath), recentFiles, calcIndices);
324
+ }),
325
+ ...filenameContainsMatches.map(f => scoreFileMatch(f, 1000, query, calcFrec(f.relativePath), recentFiles, calcIndices)),
326
+ ...pathContainsMatches.map(f => scoreFileMatch(f, 500, query, calcFrec(f.relativePath), recentFiles, calcIndices)),
327
+ ...this.performFuzzySearch(otherFiles, searchQuery, prefixMatches.length + filenameContainsMatches.length, recentFiles),
328
+ ];
329
+
330
+ scored.sort((a, b) => b.score - a.score);
331
+
332
+ if (searchQuery.length >= 2 && scored.length > 0) {
333
+ const minScoreThreshold = Math.max(scored[0].score * 0.05, 10);
334
+ return scored.filter(m => m.score >= minScoreThreshold);
335
+ }
336
+
337
+ return scored;
338
+ }
339
+
340
+ /**
341
+ * Perform fuzzy search when few good matches exist
342
+ */
343
+ private performFuzzySearch(otherFiles: FileMetadata[], searchQuery: string, goodMatchCount: number, recentFiles: Set<string>): ScoredMatch[] {
344
+ if (goodMatchCount >= 3 || otherFiles.length === 0) return [];
345
+
346
+ const query = searchQuery.toLowerCase();
347
+ const threshold = query.length <= 2 ? 0.2 : query.length <= 4 ? 0.3 : 0.35;
348
+ const fuse = new Fuse(otherFiles, {
349
+ keys: [
350
+ { name: 'fileName', weight: 0.95 },
351
+ { name: 'relativePath', weight: 0.05 }
352
+ ],
353
+ includeScore: true,
354
+ includeMatches: true,
355
+ threshold,
356
+ ignoreLocation: true,
357
+ minMatchCharLength: 2,
358
+ findAllMatches: false
359
+ });
360
+
361
+ return fuse.search(searchQuery).slice(0, 5).map(result => {
362
+ const fuseScore = result.score !== undefined ? (1 - result.score) * 200 : 0;
363
+ return {
364
+ relativePath: result.item.relativePath,
365
+ isDirectory: result.item.isDirectory,
366
+ score: fuseScore + this.calculateFrecencyScore(result.item.relativePath) - (result.item.depth * 10),
367
+ matchedIndices: extractFuseMatchIndices(result),
368
+ isRecent: recentFiles.has(result.item.relativePath)
369
+ };
370
+ });
371
+ }
372
+
373
+ /**
374
+ * Get direct contents of a directory with enhanced metadata
375
+ */
376
+ private getDirectoryContentsEnhanced(
377
+ dirPath: string,
378
+ workingDir: string,
379
+ gitignorePatterns: string[]
380
+ ): AutocompleteResult[] {
381
+ try {
382
+ const targetDir = join(workingDir, dirPath);
383
+
384
+ if (!existsSync(targetDir) || !statSync(targetDir).isDirectory()) {
385
+ return [];
386
+ }
387
+
388
+ const entries = readdirSync(targetDir, { withFileTypes: true });
389
+
390
+ const results: Array<{
391
+ value: string;
392
+ label: string;
393
+ isDir: boolean;
394
+ frecency: number;
395
+ fileType: string;
396
+ }> = [];
397
+
398
+ const SKIP_DIRS = new Set(['node_modules', 'dist', 'dist-ssr', '.git', '__pycache__', '.next', '.turbo', 'build', '.cache', 'coverage']);
399
+ for (const entry of entries) {
400
+ const relativePath = dirPath + entry.name;
401
+
402
+ if (!shouldIncludeEntry(entry, relativePath, gitignorePatterns, SKIP_DIRS)) {
403
+ continue;
404
+ }
405
+
406
+ const isDir = entry.isDirectory();
407
+ const displayPath = isDir ? `${relativePath}/` : relativePath;
408
+
409
+ results.push({
410
+ value: displayPath,
411
+ label: displayPath,
412
+ isDir,
413
+ frecency: this.calculateFrecencyScore(relativePath),
414
+ fileType: isDir ? 'directory' : getFileType(relativePath)
415
+ });
416
+ }
417
+
418
+ results.sort((a, b) => {
419
+ if (a.isDir && !b.isDir) return -1;
420
+ if (!a.isDir && b.isDir) return 1;
421
+ if (b.frecency !== a.frecency) return b.frecency - a.frecency;
422
+ return a.label.localeCompare(b.label);
423
+ });
424
+
425
+ return results.slice(0, 20).map(r => ({
426
+ value: r.value,
427
+ label: r.label,
428
+ isDirectory: r.isDir,
429
+ isRecent: r.frecency > 0,
430
+ fileType: r.fileType,
431
+ matchedIndices: []
432
+ }));
433
+ } catch (error) {
434
+ console.error('[AutocompleteService] Error getting directory contents:', error);
435
+ return [];
436
+ }
437
+ }
438
+ }