@sylphx/flow 1.0.1 → 1.0.3

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 (229) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +10 -9
  3. package/src/commands/codebase-command.ts +168 -0
  4. package/src/commands/flow-command.ts +1137 -0
  5. package/src/commands/flow-orchestrator.ts +296 -0
  6. package/src/commands/hook-command.ts +444 -0
  7. package/src/commands/init-command.ts +92 -0
  8. package/src/commands/init-core.ts +322 -0
  9. package/src/commands/knowledge-command.ts +161 -0
  10. package/src/commands/run-command.ts +120 -0
  11. package/src/components/benchmark-monitor.tsx +331 -0
  12. package/src/components/reindex-progress.tsx +261 -0
  13. package/src/composables/functional/index.ts +14 -0
  14. package/src/composables/functional/useEnvironment.ts +171 -0
  15. package/src/composables/functional/useFileSystem.ts +139 -0
  16. package/src/composables/index.ts +5 -0
  17. package/src/composables/useEnv.ts +13 -0
  18. package/src/composables/useRuntimeConfig.ts +27 -0
  19. package/src/composables/useTargetConfig.ts +45 -0
  20. package/src/config/ai-config.ts +376 -0
  21. package/src/config/constants.ts +35 -0
  22. package/src/config/index.ts +27 -0
  23. package/src/config/rules.ts +43 -0
  24. package/src/config/servers.ts +371 -0
  25. package/src/config/targets.ts +126 -0
  26. package/src/core/agent-loader.ts +141 -0
  27. package/src/core/agent-manager.ts +174 -0
  28. package/src/core/ai-sdk.ts +603 -0
  29. package/src/core/app-factory.ts +381 -0
  30. package/src/core/builtin-agents.ts +9 -0
  31. package/src/core/command-system.ts +550 -0
  32. package/src/core/config-system.ts +550 -0
  33. package/src/core/connection-pool.ts +390 -0
  34. package/src/core/di-container.ts +155 -0
  35. package/src/core/error-handling.ts +519 -0
  36. package/src/core/formatting/bytes.test.ts +115 -0
  37. package/src/core/formatting/bytes.ts +64 -0
  38. package/src/core/functional/async.ts +313 -0
  39. package/src/core/functional/either.ts +109 -0
  40. package/src/core/functional/error-handler.ts +135 -0
  41. package/src/core/functional/error-types.ts +311 -0
  42. package/src/core/functional/index.ts +19 -0
  43. package/src/core/functional/option.ts +142 -0
  44. package/src/core/functional/pipe.ts +189 -0
  45. package/src/core/functional/result.ts +204 -0
  46. package/src/core/functional/validation.ts +138 -0
  47. package/src/core/headless-display.ts +96 -0
  48. package/src/core/index.ts +6 -0
  49. package/src/core/installers/file-installer.ts +303 -0
  50. package/src/core/installers/mcp-installer.ts +213 -0
  51. package/src/core/interfaces/index.ts +22 -0
  52. package/src/core/interfaces/repository.interface.ts +91 -0
  53. package/src/core/interfaces/service.interface.ts +133 -0
  54. package/src/core/interfaces.ts +129 -0
  55. package/src/core/loop-controller.ts +200 -0
  56. package/src/core/result.ts +351 -0
  57. package/src/core/rule-loader.ts +147 -0
  58. package/src/core/rule-manager.ts +240 -0
  59. package/src/core/service-config.ts +252 -0
  60. package/src/core/session-service.ts +121 -0
  61. package/src/core/state-detector.ts +389 -0
  62. package/src/core/storage-factory.ts +115 -0
  63. package/src/core/stream-handler.ts +288 -0
  64. package/src/core/target-manager.ts +161 -0
  65. package/src/core/type-utils.ts +427 -0
  66. package/src/core/unified-storage.ts +456 -0
  67. package/src/core/upgrade-manager.ts +300 -0
  68. package/src/core/validation/limit.test.ts +155 -0
  69. package/src/core/validation/limit.ts +46 -0
  70. package/src/core/validation/query.test.ts +44 -0
  71. package/src/core/validation/query.ts +20 -0
  72. package/src/db/auto-migrate.ts +322 -0
  73. package/src/db/base-database-client.ts +144 -0
  74. package/src/db/cache-db.ts +218 -0
  75. package/src/db/cache-schema.ts +75 -0
  76. package/src/db/database.ts +70 -0
  77. package/src/db/index.ts +252 -0
  78. package/src/db/memory-db.ts +153 -0
  79. package/src/db/memory-schema.ts +29 -0
  80. package/src/db/schema.ts +289 -0
  81. package/src/db/session-repository.ts +733 -0
  82. package/src/domains/codebase/index.ts +5 -0
  83. package/src/domains/codebase/tools.ts +139 -0
  84. package/src/domains/index.ts +8 -0
  85. package/src/domains/knowledge/index.ts +10 -0
  86. package/src/domains/knowledge/resources.ts +537 -0
  87. package/src/domains/knowledge/tools.ts +174 -0
  88. package/src/domains/utilities/index.ts +6 -0
  89. package/src/domains/utilities/time/index.ts +5 -0
  90. package/src/domains/utilities/time/tools.ts +291 -0
  91. package/src/index.ts +211 -0
  92. package/src/services/agent-service.ts +273 -0
  93. package/src/services/claude-config-service.ts +252 -0
  94. package/src/services/config-service.ts +258 -0
  95. package/src/services/evaluation-service.ts +271 -0
  96. package/src/services/functional/evaluation-logic.ts +296 -0
  97. package/src/services/functional/file-processor.ts +273 -0
  98. package/src/services/functional/index.ts +12 -0
  99. package/src/services/index.ts +13 -0
  100. package/src/services/mcp-service.ts +432 -0
  101. package/src/services/memory.service.ts +476 -0
  102. package/src/services/search/base-indexer.ts +156 -0
  103. package/src/services/search/codebase-indexer-types.ts +38 -0
  104. package/src/services/search/codebase-indexer.ts +647 -0
  105. package/src/services/search/embeddings-provider.ts +455 -0
  106. package/src/services/search/embeddings.ts +316 -0
  107. package/src/services/search/functional-indexer.ts +323 -0
  108. package/src/services/search/index.ts +27 -0
  109. package/src/services/search/indexer.ts +380 -0
  110. package/src/services/search/knowledge-indexer.ts +422 -0
  111. package/src/services/search/semantic-search.ts +244 -0
  112. package/src/services/search/tfidf.ts +559 -0
  113. package/src/services/search/unified-search-service.ts +888 -0
  114. package/src/services/smart-config-service.ts +385 -0
  115. package/src/services/storage/cache-storage.ts +487 -0
  116. package/src/services/storage/drizzle-storage.ts +581 -0
  117. package/src/services/storage/index.ts +15 -0
  118. package/src/services/storage/lancedb-vector-storage.ts +494 -0
  119. package/src/services/storage/memory-storage.ts +268 -0
  120. package/src/services/storage/separated-storage.ts +467 -0
  121. package/src/services/storage/vector-storage.ts +13 -0
  122. package/src/shared/agents/index.ts +63 -0
  123. package/src/shared/files/index.ts +99 -0
  124. package/src/shared/index.ts +32 -0
  125. package/src/shared/logging/index.ts +24 -0
  126. package/src/shared/processing/index.ts +153 -0
  127. package/src/shared/types/index.ts +25 -0
  128. package/src/targets/claude-code.ts +574 -0
  129. package/src/targets/functional/claude-code-logic.ts +185 -0
  130. package/src/targets/functional/index.ts +6 -0
  131. package/src/targets/opencode.ts +529 -0
  132. package/src/types/agent.types.ts +32 -0
  133. package/src/types/api/batch.ts +108 -0
  134. package/src/types/api/errors.ts +118 -0
  135. package/src/types/api/index.ts +55 -0
  136. package/src/types/api/requests.ts +76 -0
  137. package/src/types/api/responses.ts +180 -0
  138. package/src/types/api/websockets.ts +85 -0
  139. package/src/types/api.types.ts +9 -0
  140. package/src/types/benchmark.ts +49 -0
  141. package/src/types/cli.types.ts +87 -0
  142. package/src/types/common.types.ts +35 -0
  143. package/src/types/database.types.ts +510 -0
  144. package/src/types/mcp-config.types.ts +448 -0
  145. package/src/types/mcp.types.ts +69 -0
  146. package/src/types/memory-types.ts +63 -0
  147. package/src/types/provider.types.ts +28 -0
  148. package/src/types/rule.types.ts +24 -0
  149. package/src/types/session.types.ts +214 -0
  150. package/src/types/target-config.types.ts +295 -0
  151. package/src/types/target.types.ts +140 -0
  152. package/src/types/todo.types.ts +25 -0
  153. package/src/types.ts +40 -0
  154. package/src/utils/advanced-tokenizer.ts +191 -0
  155. package/src/utils/agent-enhancer.ts +114 -0
  156. package/src/utils/ai-model-fetcher.ts +19 -0
  157. package/src/utils/async-file-operations.ts +516 -0
  158. package/src/utils/audio-player.ts +345 -0
  159. package/src/utils/cli-output.ts +266 -0
  160. package/src/utils/codebase-helpers.ts +211 -0
  161. package/src/utils/console-ui.ts +79 -0
  162. package/src/utils/database-errors.ts +140 -0
  163. package/src/utils/debug-logger.ts +49 -0
  164. package/src/utils/error-handler.ts +53 -0
  165. package/src/utils/file-operations.ts +310 -0
  166. package/src/utils/file-scanner.ts +259 -0
  167. package/src/utils/functional/array.ts +355 -0
  168. package/src/utils/functional/index.ts +15 -0
  169. package/src/utils/functional/object.ts +279 -0
  170. package/src/utils/functional/string.ts +281 -0
  171. package/src/utils/functional.ts +543 -0
  172. package/src/utils/help.ts +20 -0
  173. package/src/utils/immutable-cache.ts +106 -0
  174. package/src/utils/index.ts +78 -0
  175. package/src/utils/jsonc.ts +158 -0
  176. package/src/utils/logger.ts +396 -0
  177. package/src/utils/mcp-config.ts +249 -0
  178. package/src/utils/memory-tui.ts +414 -0
  179. package/src/utils/models-dev.ts +91 -0
  180. package/src/utils/notifications.ts +169 -0
  181. package/src/utils/object-utils.ts +51 -0
  182. package/src/utils/parallel-operations.ts +487 -0
  183. package/src/utils/paths.ts +143 -0
  184. package/src/utils/process-manager.ts +155 -0
  185. package/src/utils/prompts.ts +120 -0
  186. package/src/utils/search-tool-builder.ts +214 -0
  187. package/src/utils/secret-utils.ts +179 -0
  188. package/src/utils/security.ts +537 -0
  189. package/src/utils/session-manager.ts +168 -0
  190. package/src/utils/session-title.ts +87 -0
  191. package/src/utils/settings.ts +182 -0
  192. package/src/utils/simplified-errors.ts +410 -0
  193. package/src/utils/sync-utils.ts +159 -0
  194. package/src/utils/target-config.ts +570 -0
  195. package/src/utils/target-utils.ts +394 -0
  196. package/src/utils/template-engine.ts +94 -0
  197. package/src/utils/test-audio.ts +71 -0
  198. package/src/utils/todo-context.ts +46 -0
  199. package/src/utils/token-counter.ts +288 -0
  200. package/dist/index.d.ts +0 -10
  201. package/dist/index.js +0 -59554
  202. package/dist/lancedb.linux-x64-gnu-b7f0jgsz.node +0 -0
  203. package/dist/lancedb.linux-x64-musl-tgcv22rx.node +0 -0
  204. package/dist/shared/chunk-25dwp0dp.js +0 -89
  205. package/dist/shared/chunk-3pjb6063.js +0 -208
  206. package/dist/shared/chunk-4d6ydpw7.js +0 -2854
  207. package/dist/shared/chunk-4wjcadjk.js +0 -225
  208. package/dist/shared/chunk-5j4w74t6.js +0 -30
  209. package/dist/shared/chunk-5j8m3dh3.js +0 -58
  210. package/dist/shared/chunk-5thh3qem.js +0 -91
  211. package/dist/shared/chunk-6g9xy73m.js +0 -252
  212. package/dist/shared/chunk-7eq34c42.js +0 -23
  213. package/dist/shared/chunk-c2gwgx3r.js +0 -115
  214. package/dist/shared/chunk-cjd3mk4c.js +0 -1320
  215. package/dist/shared/chunk-g5cv6703.js +0 -368
  216. package/dist/shared/chunk-hpkhykhq.js +0 -574
  217. package/dist/shared/chunk-m2322pdk.js +0 -122
  218. package/dist/shared/chunk-nd5fdvaq.js +0 -26
  219. package/dist/shared/chunk-pgd3m6zf.js +0 -108
  220. package/dist/shared/chunk-qk8n91hw.js +0 -494
  221. package/dist/shared/chunk-rkkn8szp.js +0 -16855
  222. package/dist/shared/chunk-t16rfxh0.js +0 -61
  223. package/dist/shared/chunk-t4fbfa5v.js +0 -19
  224. package/dist/shared/chunk-t77h86w6.js +0 -276
  225. package/dist/shared/chunk-v0ez4aef.js +0 -71
  226. package/dist/shared/chunk-v29j2r3s.js +0 -32051
  227. package/dist/shared/chunk-vfbc6ew5.js +0 -765
  228. package/dist/shared/chunk-vmeqwm1c.js +0 -204
  229. package/dist/shared/chunk-x66eh37x.js +0 -137
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Standardized file operations utilities
3
+ * Provides consistent async file operations with proper error handling
4
+ */
5
+
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import { pathSecurity } from './security.js';
9
+ import { formatFileSize as formatFileSizeCore } from '../core/formatting/bytes.js';
10
+
11
+ export interface FileReadOptions {
12
+ encoding?: BufferEncoding;
13
+ fallback?: string;
14
+ }
15
+
16
+ export interface FileWriteOptions {
17
+ encoding?: BufferEncoding;
18
+ createDir?: boolean;
19
+ backup?: boolean;
20
+ }
21
+
22
+ export interface FileCopyOptions {
23
+ overwrite?: boolean;
24
+ createDir?: boolean;
25
+ }
26
+
27
+ export interface FileInfo {
28
+ exists: boolean;
29
+ isFile: boolean;
30
+ isDirectory: boolean;
31
+ size?: number;
32
+ mtime?: Date;
33
+ atime?: Date;
34
+ ctime?: Date;
35
+ }
36
+
37
+ /**
38
+ * Safely read a file with encoding and fallback options
39
+ */
40
+ export async function readFileSafe(
41
+ filePath: string,
42
+ options: FileReadOptions = {}
43
+ ): Promise<string | null> {
44
+ const { encoding = 'utf8', fallback } = options;
45
+
46
+ try {
47
+ const content = await fs.readFile(filePath, encoding);
48
+ return content;
49
+ } catch (error) {
50
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT' && fallback !== undefined) {
51
+ return fallback;
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Safely write a file with directory creation and backup options
59
+ */
60
+ export async function writeFileSafe(
61
+ filePath: string,
62
+ content: string,
63
+ options: FileWriteOptions = {}
64
+ ): Promise<void> {
65
+ const { encoding = 'utf8', createDir = true, backup = false } = options;
66
+
67
+ // Validate path security
68
+ pathSecurity.validatePath(filePath);
69
+
70
+ // Create directory if needed
71
+ if (createDir) {
72
+ const dir = path.dirname(filePath);
73
+ await ensureDirectory(dir);
74
+ }
75
+
76
+ // Create backup if requested and file exists
77
+ if (backup && (await fileExists(filePath))) {
78
+ const backupPath = `${filePath}.backup.${Date.now()}`;
79
+ await fs.copyFile(filePath, backupPath);
80
+ }
81
+
82
+ await fs.writeFile(filePath, content, encoding);
83
+ }
84
+
85
+ /**
86
+ * Check if a file or directory exists
87
+ */
88
+ export async function fileExists(filePath: string): Promise<boolean> {
89
+ try {
90
+ await fs.access(filePath);
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Get comprehensive file information
99
+ */
100
+ export async function getFileInfo(filePath: string): Promise<FileInfo> {
101
+ try {
102
+ const stats = await fs.stat(filePath);
103
+ return {
104
+ exists: true,
105
+ isFile: stats.isFile(),
106
+ isDirectory: stats.isDirectory(),
107
+ size: stats.size,
108
+ mtime: stats.mtime,
109
+ atime: stats.atime,
110
+ ctime: stats.ctime,
111
+ };
112
+ } catch {
113
+ return {
114
+ exists: false,
115
+ isFile: false,
116
+ isDirectory: false,
117
+ };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Ensure a directory exists, creating it if necessary
123
+ */
124
+ export async function ensureDirectory(dirPath: string): Promise<void> {
125
+ try {
126
+ await fs.access(dirPath);
127
+ } catch {
128
+ await fs.mkdir(dirPath, { recursive: true });
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Safely copy a file with options
134
+ */
135
+ export async function copyFileSafe(
136
+ sourcePath: string,
137
+ destPath: string,
138
+ options: FileCopyOptions = {}
139
+ ): Promise<void> {
140
+ const { overwrite = false, createDir = true } = options;
141
+
142
+ // Validate path security
143
+ pathSecurity.validatePath(sourcePath);
144
+ pathSecurity.validatePath(destPath);
145
+
146
+ // Check if source exists
147
+ if (!(await fileExists(sourcePath))) {
148
+ throw new Error(`Source file does not exist: ${sourcePath}`);
149
+ }
150
+
151
+ // Check if destination exists and overwrite is false
152
+ if (!overwrite && (await fileExists(destPath))) {
153
+ throw new Error(`Destination file already exists: ${destPath}`);
154
+ }
155
+
156
+ // Create destination directory if needed
157
+ if (createDir) {
158
+ const destDir = path.dirname(destPath);
159
+ await ensureDirectory(destDir);
160
+ }
161
+
162
+ await fs.copyFile(sourcePath, destPath);
163
+ }
164
+
165
+ /**
166
+ * Safely delete a file or directory
167
+ */
168
+ export async function deletePathSafe(targetPath: string): Promise<void> {
169
+ // Validate path security
170
+ pathSecurity.validatePath(targetPath);
171
+
172
+ if (!(await fileExists(targetPath))) {
173
+ return; // Already deleted
174
+ }
175
+
176
+ const info = await getFileInfo(targetPath);
177
+
178
+ if (info.isDirectory) {
179
+ await fs.rm(targetPath, { recursive: true, force: true });
180
+ } else {
181
+ await fs.unlink(targetPath);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Read directory contents safely
187
+ */
188
+ export async function readDirectorySafe(
189
+ dirPath: string,
190
+ options: { recursive?: boolean; includeFiles?: boolean; includeDirectories?: boolean } = {}
191
+ ): Promise<string[]> {
192
+ const { recursive = false, includeFiles = true, includeDirectories = true } = options;
193
+
194
+ // Validate path security
195
+ pathSecurity.validatePath(dirPath);
196
+
197
+ if (!(await fileExists(dirPath))) {
198
+ throw new Error(`Directory does not exist: ${dirPath}`);
199
+ }
200
+
201
+ const info = await getFileInfo(dirPath);
202
+ if (!info.isDirectory) {
203
+ throw new Error(`Path is not a directory: ${dirPath}`);
204
+ }
205
+
206
+ if (recursive) {
207
+ const results: string[] = [];
208
+ const items = await fs.readdir(dirPath, { withFileTypes: true });
209
+
210
+ for (const item of items) {
211
+ const fullPath = path.join(dirPath, item.name);
212
+
213
+ if (item.isDirectory() && includeDirectories) {
214
+ results.push(fullPath);
215
+ const subResults = await readDirectorySafe(fullPath, options);
216
+ results.push(...subResults);
217
+ } else if (item.isFile() && includeFiles) {
218
+ results.push(fullPath);
219
+ }
220
+ }
221
+
222
+ return results;
223
+ }
224
+ const items = await fs.readdir(dirPath, { withFileTypes: true });
225
+ return items
226
+ .filter((item) => {
227
+ if (item.isFile() && includeFiles) {
228
+ return true;
229
+ }
230
+ if (item.isDirectory() && includeDirectories) {
231
+ return true;
232
+ }
233
+ return false;
234
+ })
235
+ .map((item) => path.join(dirPath, item.name));
236
+ }
237
+
238
+ /**
239
+ * Find files matching patterns in a directory
240
+ */
241
+ export async function findFiles(
242
+ dirPath: string,
243
+ patterns: string[],
244
+ options: { recursive?: boolean; caseSensitive?: boolean } = {}
245
+ ): Promise<string[]> {
246
+ const { recursive = true, caseSensitive = true } = options;
247
+
248
+ const allFiles = await readDirectorySafe(dirPath, {
249
+ recursive,
250
+ includeFiles: true,
251
+ includeDirectories: false,
252
+ });
253
+
254
+ const regexPatterns = patterns.map((pattern) => {
255
+ const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.');
256
+ return new RegExp(regexPattern, caseSensitive ? '' : 'i');
257
+ });
258
+
259
+ return allFiles.filter((filePath) => {
260
+ const fileName = path.basename(filePath);
261
+ return regexPatterns.some((regex) => regex.test(fileName));
262
+ });
263
+ }
264
+
265
+ /**
266
+ * Move a file safely with validation
267
+ */
268
+ export async function moveFileSafe(
269
+ sourcePath: string,
270
+ destPath: string,
271
+ options: { overwrite?: boolean; createDir?: boolean } = {}
272
+ ): Promise<void> {
273
+ const { overwrite = false, createDir = true } = options;
274
+
275
+ // Copy first, then delete original
276
+ await copyFileSafe(sourcePath, destPath, { overwrite, createDir });
277
+ await deletePathSafe(sourcePath);
278
+ }
279
+
280
+ /**
281
+ * Get file size in human readable format
282
+ */
283
+ export function formatFileSize(bytes: number): string {
284
+ return formatFileSizeCore(bytes);
285
+ }
286
+
287
+ /**
288
+ * Validate file path against security constraints
289
+ */
290
+ export function validateFilePath(filePath: string, allowedBasePaths?: string[]): boolean {
291
+ try {
292
+ pathSecurity.validatePath(filePath);
293
+
294
+ if (allowedBasePaths) {
295
+ const resolved = path.resolve(filePath);
296
+ const isAllowed = allowedBasePaths.some((basePath) => {
297
+ const resolvedBase = path.resolve(basePath);
298
+ return resolved.startsWith(resolvedBase);
299
+ });
300
+
301
+ if (!isAllowed) {
302
+ return false;
303
+ }
304
+ }
305
+
306
+ return true;
307
+ } catch {
308
+ return false;
309
+ }
310
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * File Scanner
3
+ * Scan project files for @file auto-completion with caching
4
+ */
5
+
6
+ import { readdir, stat, readFile as fsReadFile, writeFile } from 'node:fs/promises';
7
+ import { join, relative } from 'node:path';
8
+ import { readFile } from 'node:fs/promises';
9
+ import { homedir } from 'node:os';
10
+
11
+ export interface FileInfo {
12
+ path: string;
13
+ relativePath: string;
14
+ size: number;
15
+ }
16
+
17
+ // Default ignore patterns
18
+ const DEFAULT_IGNORE = [
19
+ 'node_modules',
20
+ '.git',
21
+ 'dist',
22
+ 'build',
23
+ '.next',
24
+ '.vercel',
25
+ '.turbo',
26
+ 'coverage',
27
+ '.cache',
28
+ '.sylphx',
29
+ 'bun.lock',
30
+ 'package-lock.json',
31
+ 'yarn.lock',
32
+ ];
33
+
34
+ /**
35
+ * Load .gitignore patterns
36
+ */
37
+ async function loadGitignore(rootPath: string): Promise<Set<string>> {
38
+ const patterns = new Set<string>(DEFAULT_IGNORE);
39
+
40
+ try {
41
+ const gitignorePath = join(rootPath, '.gitignore');
42
+ const content = await readFile(gitignorePath, 'utf8');
43
+
44
+ // Parse gitignore file
45
+ for (const line of content.split('\n')) {
46
+ const trimmed = line.trim();
47
+ // Skip empty lines and comments
48
+ if (trimmed && !trimmed.startsWith('#')) {
49
+ // Remove trailing slashes
50
+ const pattern = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed;
51
+ patterns.add(pattern);
52
+ }
53
+ }
54
+ } catch {
55
+ // No .gitignore file, use defaults only
56
+ }
57
+
58
+ return patterns;
59
+ }
60
+
61
+ /**
62
+ * Check if path should be ignored
63
+ */
64
+ function shouldIgnore(relativePath: string, patterns: Set<string>): boolean {
65
+ // Check if any part of the path matches ignore patterns
66
+ const parts = relativePath.split('/');
67
+
68
+ for (const pattern of patterns) {
69
+ // Exact match
70
+ if (relativePath === pattern) return true;
71
+
72
+ // Directory match
73
+ if (parts.includes(pattern)) return true;
74
+
75
+ // Glob pattern (basic support for *)
76
+ if (pattern.includes('*')) {
77
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
78
+ if (regex.test(relativePath)) return true;
79
+ }
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Recursively scan directory for files (parallelized)
87
+ */
88
+ async function scanDirectory(
89
+ dirPath: string,
90
+ rootPath: string,
91
+ patterns: Set<string>,
92
+ results: FileInfo[] = []
93
+ ): Promise<FileInfo[]> {
94
+ try {
95
+ const entries = await readdir(dirPath, { withFileTypes: true });
96
+
97
+ // Separate files and directories for parallel processing
98
+ const files: typeof entries = [];
99
+ const directories: typeof entries = [];
100
+
101
+ for (const entry of entries) {
102
+ const fullPath = join(dirPath, entry.name);
103
+ const relativePath = relative(rootPath, fullPath);
104
+
105
+ // Skip ignored paths
106
+ if (shouldIgnore(relativePath, patterns)) {
107
+ continue;
108
+ }
109
+
110
+ if (entry.isDirectory()) {
111
+ directories.push(entry);
112
+ } else if (entry.isFile()) {
113
+ files.push(entry);
114
+ }
115
+ }
116
+
117
+ // Process files in parallel (no need to stat each one individually)
118
+ // Use entry.isFile() which we already know, skip stat() call for performance
119
+ const fileResults = files.map((entry) => {
120
+ const fullPath = join(dirPath, entry.name);
121
+ const relativePath = relative(rootPath, fullPath);
122
+ return {
123
+ path: fullPath,
124
+ relativePath,
125
+ size: 0, // We skip stat() for performance, size not critical for autocomplete
126
+ };
127
+ });
128
+ results.push(...fileResults);
129
+
130
+ // Process subdirectories in parallel
131
+ if (directories.length > 0) {
132
+ const subdirResults = await Promise.all(
133
+ directories.map((entry) => {
134
+ const fullPath = join(dirPath, entry.name);
135
+ return scanDirectory(fullPath, rootPath, patterns, []);
136
+ })
137
+ );
138
+
139
+ // Flatten results from all subdirectories
140
+ for (const subdirResult of subdirResults) {
141
+ results.push(...subdirResult);
142
+ }
143
+ }
144
+ } catch (error) {
145
+ // Skip directories we can't read
146
+ }
147
+
148
+ return results;
149
+ }
150
+
151
+ // Cache for scanned files
152
+ const CACHE_DIR = join(homedir(), '.sylphx', 'cache');
153
+ const CACHE_VERSION = 1;
154
+
155
+ interface ScanCache {
156
+ version: number;
157
+ rootPath: string;
158
+ timestamp: number;
159
+ files: FileInfo[];
160
+ }
161
+
162
+ /**
163
+ * Get cache file path for a project
164
+ */
165
+ function getCachePath(rootPath: string): string {
166
+ // Use hash of root path as cache filename
167
+ const hash = Buffer.from(rootPath).toString('base64').replace(/[/+=]/g, '_');
168
+ return join(CACHE_DIR, `filescan-${hash}.json`);
169
+ }
170
+
171
+ /**
172
+ * Load cached file list if valid
173
+ */
174
+ async function loadCache(rootPath: string): Promise<FileInfo[] | null> {
175
+ try {
176
+ const cachePath = getCachePath(rootPath);
177
+ const content = await fsReadFile(cachePath, 'utf8');
178
+ const cache: ScanCache = JSON.parse(content);
179
+
180
+ // Validate cache
181
+ if (cache.version !== CACHE_VERSION || cache.rootPath !== rootPath) {
182
+ return null;
183
+ }
184
+
185
+ // Check if cache is still fresh (less than 5 minutes old)
186
+ const age = Date.now() - cache.timestamp;
187
+ const MAX_CACHE_AGE = 5 * 60 * 1000; // 5 minutes
188
+ if (age > MAX_CACHE_AGE) {
189
+ return null;
190
+ }
191
+
192
+ return cache.files;
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Save file list to cache
200
+ */
201
+ async function saveCache(rootPath: string, files: FileInfo[]): Promise<void> {
202
+ try {
203
+ const cachePath = getCachePath(rootPath);
204
+ const cache: ScanCache = {
205
+ version: CACHE_VERSION,
206
+ rootPath,
207
+ timestamp: Date.now(),
208
+ files,
209
+ };
210
+
211
+ // Ensure cache directory exists
212
+ const { mkdir } = await import('node:fs/promises');
213
+ await mkdir(CACHE_DIR, { recursive: true });
214
+
215
+ // Write cache file
216
+ await writeFile(cachePath, JSON.stringify(cache), 'utf8');
217
+ } catch (error) {
218
+ // Ignore cache write errors
219
+ console.warn('Failed to write file scanner cache:', error);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Scan project files with caching
225
+ * Returns list of files respecting .gitignore
226
+ */
227
+ export async function scanProjectFiles(rootPath: string): Promise<FileInfo[]> {
228
+ // Try to load from cache first
229
+ const cached = await loadCache(rootPath);
230
+ if (cached) {
231
+ return cached;
232
+ }
233
+
234
+ // Cache miss or stale, scan filesystem
235
+ const patterns = await loadGitignore(rootPath);
236
+ const files = await scanDirectory(rootPath, rootPath, patterns);
237
+
238
+ // Sort by path for consistent ordering
239
+ files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
240
+
241
+ // Save to cache for next time
242
+ saveCache(rootPath, files).catch(() => {
243
+ // Ignore cache save errors
244
+ });
245
+
246
+ return files;
247
+ }
248
+
249
+ /**
250
+ * Filter files by query string
251
+ */
252
+ export function filterFiles(files: FileInfo[], query: string): FileInfo[] {
253
+ if (!query) return files;
254
+
255
+ const lowerQuery = query.toLowerCase();
256
+ return files.filter((file) =>
257
+ file.relativePath.toLowerCase().includes(lowerQuery)
258
+ );
259
+ }