import Foundation enum SyncAuthMethod: String, CaseIterable, Codable { case ssh = "ssh" case password = "password" case token = "token" var displayName: String { switch self { case .ssh: return "SSH Key" case .password: return "Username + Password" case .token: return "Access Token" } } } enum SyncError: LocalizedError { case notConfigured case missingCredentials case gitNotFound case gitFailed(String) case repoNotCloned case secretsDetected([String]) case parseError(String) var errorDescription: String? { switch self { case .notConfigured: return "Git sync not configured. Check Settings > Sync." case .missingCredentials: return "Missing credentials. Check authentication method in Settings > Sync." case .gitNotFound: return "Git not found. Install Xcode Command Line Tools." case .gitFailed(let message): return "Git command failed: \(message)" case .repoNotCloned: return "Repository not cloned. Click 'Clone Repository' first." case .secretsDetected(let secrets): return "Secrets detected in conversations: \(secrets.joined(separator: ", ")). Remove before syncing." case .parseError(let message): return "Failed to parse conversation: \(message)" } } } struct SyncStatus: Equatable { var lastSyncTime: Date? var uncommittedChanges: Int = 0 var isCloned: Bool = false var currentBranch: String? var remoteStatus: String? // "up-to-date", "ahead 3", "behind 2", etc. } struct ConversationExport { let id: String let name: String let createdAt: Date let updatedAt: Date let primaryModel: String? // Primary model used in conversation let messages: [MessageExport] struct MessageExport { let role: String let content: String let timestamp: Date let tokens: Int? let cost: Double? let modelId: String? // Model that generated this message } func toMarkdown() -> String { var md = "# \(name)\n\n" md += "**ID**: `\(id)`\n" md += "**Created**: \(ISO8601DateFormatter().string(from: createdAt))\n" md += "**Updated**: \(ISO8601DateFormatter().string(from: updatedAt))\n" // Add primary model if available if let primaryModel = primaryModel { md += "**Primary Model**: \(primaryModel)\n" } // Calculate all unique models used let uniqueModels = Set(messages.compactMap { $0.modelId }) if !uniqueModels.isEmpty { md += "**Models Used**: \(uniqueModels.sorted().joined(separator: ", "))\n" } md += "\n---\n\n" for message in messages { md += "## \(message.role.capitalized)\n\n" md += message.content + "\n\n" var meta: [String] = [] if let modelId = message.modelId { meta.append("Model: \(modelId)") } if let tokens = message.tokens { meta.append("Tokens: \(tokens)") } if let cost = message.cost { meta.append(String(format: "Cost: $%.4f", cost)) } if !meta.isEmpty { md += "*\(meta.joined(separator: " | "))*\n\n" } md += "---\n\n" } return md } /// Parse markdown back to ConversationExport static func fromMarkdown(_ markdown: String) throws -> ConversationExport { let lines = markdown.components(separatedBy: .newlines) var lineIndex = 0 // Parse title (first line should be "# Title") guard lineIndex < lines.count, lines[lineIndex].hasPrefix("# ") else { throw SyncError.parseError("Missing title") } let name = String(lines[lineIndex].dropFirst(2)).trimmingCharacters(in: .whitespaces) lineIndex += 1 // Skip empty line while lineIndex < lines.count && lines[lineIndex].trimmingCharacters(in: .whitespaces).isEmpty { lineIndex += 1 } // Parse ID guard lineIndex < lines.count, lines[lineIndex].hasPrefix("**ID**: `") else { throw SyncError.parseError("Missing ID") } let idLine = lines[lineIndex] let idStart = idLine.index(idLine.startIndex, offsetBy: 9) let idEnd = idLine.lastIndex(of: "`") ?? idLine.endIndex let id = String(idLine[idStart..