// // String+Extensions.swift // oAI // // String utility extensions // import Foundation extension String { // MARK: - Command Parsing var isSlashCommand: Bool { hasPrefix("/") } func parseCommand() -> (command: String, args: [String])? { guard isSlashCommand else { return nil } let parts = self.split(separator: " ", omittingEmptySubsequences: true) .map(String.init) guard let command = parts.first else { return nil } let args = Array(parts.dropFirst()) return (command, args) } // MARK: - File Attachment Parsing func parseFileAttachments() -> (cleanText: String, filePaths: [String]) { var cleanText = self var filePaths: [String] = [] // Pattern 1: @ let anglePattern = #"@<([^>]+)>"# if let regex = try? NSRegularExpression(pattern: anglePattern) { let matches = regex.matches(in: self, range: NSRange(self.startIndex..., in: self)) for match in matches.reversed() { if let range = Range(match.range(at: 1), in: self) { let path = String(self[range]) filePaths.insert(path, at: 0) } if let fullRange = Range(match.range, in: self) { cleanText.removeSubrange(fullRange) } } } // Pattern 2: @filepath (starting with /, ~, ., or drive letter) let directPattern = #"@([~/.][\S]+|[A-Za-z]:[\\\/][\S]+)"# if let regex = try? NSRegularExpression(pattern: directPattern) { let matches = regex.matches(in: cleanText, range: NSRange(cleanText.startIndex..., in: cleanText)) for match in matches.reversed() { if let range = Range(match.range(at: 1), in: cleanText) { let path = String(cleanText[range]) if !filePaths.contains(path) { filePaths.insert(path, at: 0) } } if let fullRange = Range(match.range, in: cleanText) { cleanText.removeSubrange(fullRange) } } } return (cleanText.trimmingCharacters(in: .whitespaces), filePaths) } // MARK: - Token Estimation func estimateTokens() -> Int { // Rough estimation: ~4 characters per token // This is approximate; Phase 2 will use proper tokenizer return max(1, count / 4) } // MARK: - Truncation func truncated(to length: Int, trailing: String = "...") -> String { if count <= length { return self } let endIndex = index(startIndex, offsetBy: length - trailing.count) return String(self[..