1→import Foundation 2→ 3→/// Main AI orchestration service for GhostWriter 4→actor GhostAIService { 5→ static let shared = GhostAIService() 6→ 7→ private let claude = ClaudeService.shared 8→ private let lmstudio = LMStudioService.shared 9→ private let styleAnalyzer = StyleAnalyzer() 10→ private let pipeline = ContentPipelineService.shared 11→ 12→ // Current provider preference 13→ private var preferredProvider: AIProvider = .claude 14→ 15→ private init() {} 16→ 17→ // MARK: - Provider Status 18→ 19→ func getAvailableProviders() async -> [AIProvider] { 20→ var providers: [AIProvider] = [] 21→ 22→ if await claude.hasAPIKey { 23→ providers.append(.claude) 24→ } 25→ 26→ if await lmstudio.isAvailable() { 27→ providers.append(.lmstudio) 28→ } 29→ 30→ return providers 31→ } 32→ 33→ func setPreferredProvider(_ provider: AIProvider) { 34→ preferredProvider = provider 35→ } 36→ 37→ // MARK: - Style Analysis 38→ 39→ func analyzeStyle(from samples: [String]) async throws -> StyleAnalyzer.StyleAnalysisResult { 40→ guard samples.count >= AppConstants.Style.minSamplesForAnalysis else { 41→ throw GhostAIError.parseError("Need at least \(AppConstants.Style.minSamplesForAnalysis) samples") 42→ } 43→ 44→ let prompt = styleAnalyzer.buildAnalysisPrompt(samples: samples) 45→ let content = try await generateWithFallback(prompt: prompt) 46→ return try styleAnalyzer.parseAnalysisResult(from: content) 47→ } 48→ 49→ // MARK: - Hook Generation 50→ 51→ func generateHooks( 52→ topic: String, 53→ style: StyleProfile?, 54→ count: Int = 5, 55→ category: HookCategory? = nil 56→ ) async throws -> [String] { 57→ var systemPrompt = "" 58→ if let style = style { 59→ systemPrompt = styleAnalyzer.buildStyleSystemPrompt(from: style) 60→ } 61→ 62→ let categoryFilter = category.map { "Category: \($0.rawValue) - \($0.description)" } ?? "Any category" 63→ 64→ let prompt = """ 65→ Generate \(count) viral hooks for social media about: \(topic) 66→ 67→ \(categoryFilter) 68→ 69→ Requirements: 70→ - Each hook should be 5-15 words 71→ - Create curiosity or tension 72→ - Make people want to read more 73→ - Varied formats (questions, statements, numbers) 74→ 75→ Return as JSON array: 76→ ["hook 1", "hook 2", "hook 3", ...] 77→ 78→ Return ONLY the JSON array. 79→ """ 80→ 81→ let content = try await generateWithFallback( 82→ prompt: prompt, 83→ systemPrompt: systemPrompt.isEmpty ? nil : systemPrompt 84→ ) 85→ 86→ return parseHooksArray(from: content) 87→ } 88→ 89→ // MARK: - Caption Generation 90→ 91→ func generateCaption( 92→ topic: String, 93→ hook: String?, 94→ style: StyleProfile, 95→ platform: Platform, 96→ includeHashtags: Bool = true 97→ ) async throws -> GeneratedContent { 98→ let stylePrompt = styleAnalyzer.buildStyleSystemPrompt(from: style) 99→ 100→ let hookSection = hook.map { "USE THIS HOOK: \($0)" } ?? "Generate an appropriate hook" 101→ 102→ let prompt = """ 103→ Write a \(platform.rawValue) post about: \(topic) 104→ 105→ \(hookSection) 106→ 107→ FORMAT: \(platform.promptContext) 108→ CHARACTER LIMIT: \(platform.characterLimit.map { String($0) } ?? "No limit") 109→ \(includeHashtags ? "HASHTAG LIMIT: \(platform.hashtagLimit)" : "NO HASHTAGS") 110→ 111→ Return in JSON format: 112→ { 113→ "hook": "Opening hook", 114→ "body": "Main content", 115→ "cta": "Call to action", 116→ "hashtags": \(includeHashtags ? "[\"tag1\", \"tag2\"]" : "[]"), 117→ "fullContent": "Complete post ready to publish" 118→ } 119→ 120→ Return ONLY valid JSON. 121→ """ 122→ 123→ let content = try await generateWithFallback( 124→ prompt: prompt, 125→ systemPrompt: stylePrompt 126→ ) 127→ 128→ return parseGeneratedContent(content, for: platform) 129→ } 130→ 131→ // MARK: - Content Pipeline (Newsletter -> All Platforms) 132→ 133→ func generateContentPipeline( 134→ topic: String, 135→ style: StyleProfile, 136→ keyPoints: [String]? = nil 137→ ) async throws -> [Platform: GeneratedContent] { 138→ // 1. Generate newsletter first 139→ let newsletter = try await pipeline.generateNewsletter( 140→ topic: topic, 141→ style: style, 142→ keyPoints: keyPoints 143→ ) 144→ 145→ // 2. Transform to all other platforms 146→ var results: [Platform: GeneratedContent] = [.newsletter: newsletter] 147→ 148→ let otherPlatforms = try await pipeline.transformToAllPlatforms( 149→ newsletter: newsletter.fullContent, 150→ style: style, 151→ topic: topic 152→ ) 153→ 154→ for (platform, content) in otherPlatforms { 155→ results[platform] = content 156→ } 157→ 158→ return results 159→ } 160→ 161→ // MARK: - Private Helpers 162→ 163→ private func generateWithFallback( 164→ prompt: String, 165→ systemPrompt: String? = nil 166→ ) async throws -> String { 167→ // Try preferred provider first 168→ do { 169→ switch preferredProvider { 170→ case .claude: 171→ if await claude.hasAPIKey { 172→ return try await claude.generate(prompt: prompt, systemPrompt: systemPrompt) 173→ } 174→ case .lmstudio: 175→ if await lmstudio.isAvailable() { 176→ return try await lmstudio.generate(prompt: prompt, systemPrompt: systemPrompt) 177→ } 178→ } 179→ } catch { 180→ print("Preferred provider \(preferredProvider.rawValue) failed: \(error)") 181→ } 182→ 183→ // Try fallback provider 184→ let fallback: AIProvider = preferredProvider == .claude ? .lmstudio : .claude 185→ 186→ switch fallback { 187→ case .claude: 188→ if await claude.hasAPIKey { 189→ return try await claude.generate(prompt: prompt, systemPrompt: systemPrompt) 190→ } 191→ case .lmstudio: 192→ if await lmstudio.isAvailable() { 193→ return try await lmstudio.generate(prompt: prompt, systemPrompt: systemPrompt) 194→ } 195→ } 196→ 197→ throw GhostAIError.missingAPIKey 198→ } 199→ 200→ private func parseHooksArray(from content: String) -> [String] { 201→ var jsonString = content 202→ .replacingOccurrences(of: "```json", with: "") 203→ .replacingOccurrences(of: "```", with: "") 204→ .trimmingCharacters(in: .whitespacesAndNewlines) 205→ 206→ if let startIndex = jsonString.firstIndex(of: "["), 207→ let endIndex = jsonString.lastIndex(of: "]") { 208→ jsonString = String(jsonString[startIndex...endIndex]) 209→ } 210→ 211→ guard let data = jsonString.data(using: .utf8), 212→ let array = try? JSONDecoder().decode([String].self, from: data) else { 213→ // Fallback: split by newlines 214→ return content 215→ .components(separatedBy: .newlines) 216→ .map { $0.trimmingCharacters(in: .whitespaces) } 217→ .filter { !$0.isEmpty } 218→ } 219→ 220→ return array 221→ } 222→ 223→ private func parseGeneratedContent(_ content: String, for platform: Platform) -> GeneratedContent { 224→ var jsonString = content 225→ .replacingOccurrences(of: "```json", with: "") 226→ .replacingOccurrences(of: "```", with: "") 227→ .trimmingCharacters(in: .whitespacesAndNewlines) 228→ 229→ if let startIndex = jsonString.firstIndex(of: "{"), 230→ let endIndex = jsonString.lastIndex(of: "}") { 231→ jsonString = String(jsonString[startIndex...endIndex]) 232→ } 233→ 234→ guard let data = jsonString.data(using: .utf8), 235→ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { 236→ return GeneratedContent( 237→ platform: platform, 238→ hook: "", 239→ body: content, 240→ cta: "", 241→ hashtags: [], 242→ fullContent: content 243→ ) 244→ } 245→ 246→ return GeneratedContent( 247→ platform: platform, 248→ hook: json["hook"] as? String ?? "", 249→ body: json["body"] as? String ?? "", 250→ cta: json["cta"] as? String ?? "", 251→ hashtags: json["hashtags"] as? [String] ?? [], 252→ fullContent: json["fullContent"] as? String ?? content 253→ ) 254→ } 255→} 256→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.