@@ -94,10 +94,9 @@ func encodeImageToDataURL(filePath string) (string, error) {
9494 return "" , err
9595 }
9696
97- contentType := http .DetectContentType (buf )
98- allowedTypes := []string {"image/jpeg" , "image/png" , "image/webp" }
99- if ! slices .Contains (allowedTypes , contentType ) {
100- return "" , fmt .Errorf ("invalid image type: %s" , contentType )
97+ // Validate that this is an image file using both content and extension
98+ if ! isImageFileByContentAndExtension (buf , filePath ) {
99+ return "" , fmt .Errorf ("invalid image type for file: %s" , filePath )
101100 }
102101
103102 info , err := file .Stat ()
@@ -158,3 +157,112 @@ func processImagesInPrompt(prompt string) (string, []string, error) {
158157
159158 return strings .TrimSpace (prompt ), imageDataURLs , nil
160159}
160+
161+ // extractFileInclusions finds file paths in the prompt text using the @ symbol
162+ // e.g., @filename.txt, @./path/to/file.txt, @/absolute/path/file.txt
163+ func extractFileInclusions (prompt string ) []string {
164+ // Regex to match @ followed by a file path
165+ // Pattern explanation:
166+ // - @ symbol before the path
167+ // - File path can be quoted (at least one char) or unquoted (at least one char)
168+ // - Supports relative (./, ../) and absolute paths
169+ regexPattern := `@(?:"([^"]+)"|'([^']+)'|([^\s"']+\b))`
170+ re := regexp .MustCompile (regexPattern )
171+ matches := re .FindAllStringSubmatch (prompt , - 1 )
172+
173+ paths := []string {}
174+ for _ , match := range matches {
175+ // match[0] is the full match
176+ // match[1] is double-quoted content
177+ // match[2] is single-quoted content
178+ // match[3] is unquoted content
179+ if len (match ) >= 2 && match [1 ] != "" {
180+ paths = append (paths , match [1 ])
181+ } else if len (match ) >= 3 && match [2 ] != "" {
182+ paths = append (paths , match [2 ])
183+ } else if len (match ) >= 4 && match [3 ] != "" {
184+ paths = append (paths , match [3 ])
185+ }
186+ }
187+
188+ return paths
189+ }
190+
191+ // hasValidImageExtension checks if the file has a valid image extension
192+ func hasValidImageExtension (filePath string ) bool {
193+ filePathLower := strings .ToLower (filePath )
194+ return strings .HasSuffix (filePathLower , ".jpg" ) ||
195+ strings .HasSuffix (filePathLower , ".jpeg" ) ||
196+ strings .HasSuffix (filePathLower , ".png" ) ||
197+ strings .HasSuffix (filePathLower , ".webp" )
198+ }
199+
200+ // isImageFileByContentAndExtension checks if a file is an image using both file extension and content type detection
201+ func isImageFileByContentAndExtension (content []byte , filePath string ) bool {
202+ // First check content type
203+ contentType := http .DetectContentType (content )
204+ if strings .HasPrefix (contentType , "image/" ) {
205+ return true
206+ }
207+
208+ // Fallback to file extension check if content detection fails
209+ return hasValidImageExtension (filePath )
210+ }
211+
212+ // processFileInclusions extracts files mentioned with @ symbol, reads their contents,
213+ // and returns the prompt with file contents embedded
214+ func processFileInclusions (prompt string ) (string , error ) {
215+ filePaths := extractFileInclusions (prompt )
216+
217+ // Process each file inclusion in order
218+ for _ , filePath := range filePaths {
219+ nfp := normalizeFilePath (filePath )
220+
221+ // Read the file content
222+ content , err := os .ReadFile (nfp )
223+ if err != nil {
224+ // Skip non-existent files or files that can't be read
225+ continue
226+ }
227+
228+ // Check if the file is an image to handle it appropriately
229+ // Only content is checked here, not file extension, to maintain original behavior
230+ if isImageFileByContentAndExtension (content , nfp ) {
231+ // For image files, we keep the original file reference (@filePath) in the prompt
232+ // so that processImagesInPrompt can handle it properly
233+ continue
234+ }
235+
236+ // For non-image files, replace the @filename with the file content
237+ // Try different variations to match how it appears in the prompt
238+ escapedPath := regexp .QuoteMeta (filePath ) // Escape special regex chars
239+ quotedPath := regexp .QuoteMeta (nfp ) // Also try normalized path
240+
241+ // Replace all occurrences of the file reference with its content
242+ contentStr := string (content )
243+
244+ // Replace @ symbol usage with the file content - preserve the space or end by using word boundaries
245+ // Use capturing groups to preserve the space
246+ prompt = regexp .MustCompile (`@` + `"` + escapedPath + `"` ).ReplaceAllString (prompt , contentStr )
247+ prompt = regexp .MustCompile (`@` + `'` + escapedPath + `'` ).ReplaceAllString (prompt , contentStr )
248+ // For the unquoted version, we need to match @ + path and replace with content + the boundary character
249+ // Use a more specific pattern that captures the space or end
250+ prompt = regexp .MustCompile (`(@` + escapedPath + `)(\s|$)` ).ReplaceAllString (prompt , contentStr + "$2" )
251+
252+ // Also try replacing with normalized path
253+ prompt = regexp .MustCompile (`@` + `"` + quotedPath + `"` ).ReplaceAllString (prompt , contentStr )
254+ prompt = regexp .MustCompile (`@` + `'` + quotedPath + `'` ).ReplaceAllString (prompt , contentStr )
255+ prompt = regexp .MustCompile (`(@` + quotedPath + `)(\s|$)` ).ReplaceAllString (prompt , contentStr + "$2" )
256+ }
257+
258+ return prompt , nil
259+ }
260+
261+ // For testing purposes - making the functions public so they can be tested
262+ func ExtractFileInclusions (prompt string ) []string {
263+ return extractFileInclusions (prompt )
264+ }
265+
266+ func ProcessFileInclusions (prompt string ) (string , error ) {
267+ return processFileInclusions (prompt )
268+ }
0 commit comments