The file /Users/neog/Apps/macOS/GhostWriter/GhostWriter/Services/AI/GhostAIService.swift has been updated. Here's the result of running `cat -n` on a snippet of the edited file: 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 styleAnalyzer = StyleAnalyzer() 9→ private let pipeline = ContentPipelineService.shared 10→ 11→ private init() {} 12→ 13→ // MARK: - Provider Status 14→ 15→ func isAvailable() async -> Bool { 16→ await claude.hasAPIKey 17→ } 18→ 19→ // MARK: - Style Analysis 20→ 21→ func analyzeStyle(from samples: [String]) async throws -> StyleAnalyzer.StyleAnalysisResult { 22→ guard samples.count >= AppConstants.Style.minSamplesForAnalysis else { 23→ throw GhostAIError.parseError("Need at least \(AppConstants.Style.minSamplesForAnalysis) samples") 24→ } 25→ 26→ let prompt = styleAnalyzer.buildAnalysisPrompt(samples: samples) 27→ let content = try await claude.generate(prompt: prompt) 28→ return try styleAnalyzer.parseAnalysisResult(from: content) 29→ } 30→ 31→ // MARK: - Hook Generation 32→ 33→ func generateHooks( 34→ topic: String, 35→ style: StyleProfile?, 36→ count: Int = 5, 37→ category: HookCategory? = nil 38→ ) async throws -> [String] { 39→ guard await claude.hasAPIKey else { 40→ throw GhostAIError.missingAPIKey 41→ } 42→ 43→ var systemPrompt = "" 44→ if let style = style { 45→ systemPrompt = styleAnalyzer.buildStyleSystemPrompt(from: style) 46→ } 47→ 48→ let categoryFilter = category.map { "Category: \($0.rawValue) - \($0.description)" } ?? "Any category" 49→ 50→ let prompt = """ 51→ Generate \(count) viral hooks for social media about: \(topic) 52→ 53→ \(categoryFilter) 54→ 55→ Requirements: 56→ - Each hook should be 5-15 words 57→ - Create curiosity or tension 58→ - Make people want to read more 59→ - Varied formats (questions, statements, numbers) 60→ 61→ Return as JSON array: 62→ ["hook 1", "hook 2", "hook 3", ...] 63→ 64→ Return ONLY the JSON array. 65→ """ 66→ 67→ let content = try await claude.generate( 68→ prompt: prompt, 69→ systemPrompt: systemPrompt.isEmpty ? nil : systemPrompt 70→ ) 71→ 72→ return parseHooksArray(from: content) 73→ } 74→ 75→ // MARK: - Caption Generation 76→ 77→ func generateCaption( 78→ topic: String, 79→ hook: String?, 80→ style: StyleProfile, 81→ platform: Platform, 82→ includeHashtags: Bool = true 83→ ) async throws -> GeneratedContent { 84→ guard await claude.hasAPIKey else { 85→ throw GhostAIError.missingAPIKey 86→ } 87→ 88→ let stylePrompt = styleAnalyzer.buildStyleSystemPrompt(from: style) 89→ 90→ let hookSection = hook.map { "USE THIS HOOK: \($0)" } ?? "Generate an appropriate hook" 91→ 92→ let prompt = """ 93→ Write a \(platform.rawValue) post about: \(topic) 94→ 95→ \(hookSection) 96→ 97→ FORMAT: \(platform.promptContext) 98→ CHARACTER LIMIT: \(platform.characterLimit.map { String($0) } ?? "No limit") 99→ \(includeHashtags ? "HASHTAG LIMIT: \(platform.hashtagLimit)" : "NO HASHTAGS") 100→ 101→ Return in JSON format: 102→ { 103→ "hook": "Opening hook", 104→ "body": "Main content", 105→ "cta": "Call to action", 106→ "hashtags": \(includeHashtags ? "[\"tag1\", \"tag2\"]" : "[]"), 107→ "fullContent": "Complete post ready to publish" 108→ } 109→ 110→ Return ONLY valid JSON. 111→ """ 112→ 113→ let content = try await claude.generate( 114→ prompt: prompt, 115→ systemPrompt: stylePrompt 116→ ) 117→ 118→ return parseGeneratedContent(content, for: platform) 119→ } 120→ 121→ // MARK: - Content Pipeline (Newsletter -> All Platforms) 122→ 123→ func generateContentPipeline( 124→ topic: String, 125→ style: StyleProfile, 126→ keyPoints: [String]? = nil 127→ ) async throws -> [Platform: GeneratedContent] { 128→ guard await claude.hasAPIKey else { 129→ throw GhostAIError.missingAPIKey 130→ } 131→ 132→ // 1. Generate newsletter first 133→ let newsletter = try await pipeline.generateNewsletter( 134→ topic: topic, 135→ style: style, 136→ keyPoints: keyPoints 137→ ) 138→ 139→ // 2. Transform to all other platforms 140→ var results: [Platform: GeneratedContent] = [.newsletter: newsletter] 141→ 142→ let otherPlatforms = try await pipeline.transformToAllPlatforms( 143→ newsletter: newsletter.fullContent, 144→ style: style, 145→ topic: topic 146→ ) 147→ 148→ for (platform, content) in otherPlatforms { 149→ results[platform] = content 150→ } 151→ 152→ return results 153→ } 154→ 155→ // MARK: - Private Helpers 156→ 157→ private func parseHooksArray(from content: String) -> [String] { 158→ var jsonString = content 159→ .replacingOccurrences(of: "```json", with: "") 160→ .replacingOccurrences(of: "```", with: "") 161→ .trimmingCharacters(in: .whitespacesAndNewlines) 162→ 163→ if let startIndex = jsonString.firstIndex(of: "["), 164→ let endIndex = jsonString.lastIndex(of: "]") { 165→ jsonString = String(jsonString[startIndex...endIndex]) 166→ } 167→ 168→ guard let data = jsonString.data(using: .utf8), 169→ let array = try? JSONDecoder().decode([String].self, from: data) else { 170→ // Fallback: split by newlines 171→ return content 172→ .components(separatedBy: .newlines) 173→ .map { $0.trimmingCharacters(in: .whitespaces) } 174→ .filter { !$0.isEmpty } 175→ } 176→ 177→ return array 178→ } 179→ 180→ private func parseGeneratedContent(_ content: String, for platform: Platform) -> GeneratedContent { 181→ var jsonString = content 182→ .replacingOccurrences(of: "```json", with: "") 183→ .replacingOccurrences(of: "```", with: "") 184→ .trimmingCharacters(in: .whitespacesAndNewlines) 185→ 186→ if let startIndex = jsonString.firstIndex(of: "{"), 187→ let endIndex = jsonString.lastIndex(of: "}") { 188→ jsonString = String(jsonString[startIndex...endIndex]) 189→ } 190→ 191→ guard let data = jsonString.data(using: .utf8), 192→ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { 193→ return GeneratedContent( 194→ platform: platform, 195→ hook: "", 196→ body: content, 197→ cta: "", 198→ hashtags: [], 199→ fullContent: content 200→ ) 201→ } 202→ 203→ return GeneratedContent( 204→ platform: platform, 205→ hook: json["hook"] as? String ?? "", 206→ body: json["body"] as? String ?? "", 207→ cta: json["cta"] as? String ?? "", 208→ hashtags: json["hashtags"] as? [String] ?? [], 209→ fullContent: json["fullContent"] as? String ?? content 210→ ) 211→ } 212→} 213→