SPB Git

spb/zyquo-local Public MIT

Native macOS AI chat that runs LLMs 100% locally on Apple Silicon with MLX — no cloud, no API keys.

Swift 97.2% Shell 1.8% Makefile 1%
2.8 KB · 77 lines swift
Raw Blame History
1//2//  PersistenceService.swift3//  Zyquo Local4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//89import Foundation1011/// Conversations, personas and settings persisted as JSON under12/// ~/Library/Application Support/ZyquoLocal/.13enum PersistenceService {14    static var appSupportDirectory: URL {15        let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]16        let dir = base.appendingPathComponent("ZyquoLocal")17        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)18        return dir19    }2021    private static var conversationsDirectory: URL {22        let dir = appSupportDirectory.appendingPathComponent("Conversations")23        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)24        return dir25    }2627    private static let encoder: JSONEncoder = {28        let e = JSONEncoder()29        e.dateEncodingStrategy = .iso860130        e.outputFormatting = [.sortedKeys]31        return e32    }()3334    private static let decoder: JSONDecoder = {35        let d = JSONDecoder()36        d.dateDecodingStrategy = .iso860137        return d38    }()3940    // MARK: - Conversations (one JSON file per conversation)4142    static func loadConversations() -> [Conversation] {43        let files = (try? FileManager.default.contentsOfDirectory(44            at: conversationsDirectory, includingPropertiesForKeys: nil)) ?? []45        return files46            .filter { $0.pathExtension == "json" }47            .compactMap { url in48                guard let data = try? Data(contentsOf: url) else { return nil }49                return try? decoder.decode(Conversation.self, from: data)50            }51            .sorted { $0.updatedAt > $1.updatedAt }52    }5354    static func save(_ conversation: Conversation) {55        let url = conversationsDirectory.appendingPathComponent("\(conversation.id.uuidString).json")56        try? encoder.encode(conversation).write(to: url, options: .atomic)57    }5859    static func delete(conversationID: UUID) {60        let url = conversationsDirectory.appendingPathComponent("\(conversationID.uuidString).json")61        try? FileManager.default.removeItem(at: url)62    }6364    // MARK: - Generic single-document storage (settings, personas, prompts)6566    static func loadDocument<T: Decodable>(_ type: T.Type, named name: String) -> T? {67        let url = appSupportDirectory.appendingPathComponent("\(name).json")68        guard let data = try? Data(contentsOf: url) else { return nil }69        return try? decoder.decode(T.self, from: data)70    }7172    static func saveDocument<T: Encodable>(_ value: T, named name: String) {73        let url = appSupportDirectory.appendingPathComponent("\(name).json")74        try? encoder.encode(value).write(to: url, options: .atomic)75    }76}77