Files
oai-swift/oAI/Services/AgentSkillFilesService.swift
T
runeandClaude Sonnet 5 bd686873c4 Update commercial licensing contact URL to oai.pm
Consolidates the mac.oai.pm subdomain references (introduced in the
PolyForm Noncommercial relicense) to the root oai.pm domain, across
the LICENSE file, README, and all Swift source file headers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:33:53 +02:00

88 lines
3.0 KiB
Swift

//
// AgentSkillFilesService.swift
// oAI
//
// Manages per-skill file directories in Application Support/oAI/skills/<uuid>/
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling oAI or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://oai.pm>.
import Foundation
import UniformTypeIdentifiers
final class AgentSkillFilesService {
static let shared = AgentSkillFilesService()
private let baseDirectory: URL = {
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory,
in: .userDomainMask).first!
return appSupport.appendingPathComponent("oAI/skills", isDirectory: true)
}()
func skillDirectory(for id: UUID) -> URL {
baseDirectory.appendingPathComponent(id.uuidString, isDirectory: true)
}
func ensureDirectory(for id: UUID) {
try? FileManager.default.createDirectory(
at: skillDirectory(for: id), withIntermediateDirectories: true)
}
func listFiles(for id: UUID) -> [URL] {
guard let contents = try? FileManager.default.contentsOfDirectory(
at: skillDirectory(for: id),
includingPropertiesForKeys: [.fileSizeKey],
options: .skipsHiddenFiles)
else { return [] }
return contents.sorted { $0.lastPathComponent < $1.lastPathComponent }
}
func addFile(from sourceURL: URL, to id: UUID) throws {
ensureDirectory(for: id)
let dest = skillDirectory(for: id).appendingPathComponent(sourceURL.lastPathComponent)
if FileManager.default.fileExists(atPath: dest.path) {
try FileManager.default.removeItem(at: dest)
}
try FileManager.default.copyItem(at: sourceURL, to: dest)
}
func deleteFile(at url: URL) {
try? FileManager.default.removeItem(at: url)
}
func deleteAll(for id: UUID) {
try? FileManager.default.removeItem(at: skillDirectory(for: id))
}
func hasFiles(for id: UUID) -> Bool {
!listFiles(for: id).isEmpty
}
/// Returns (filename, content) for all readable text files
func readTextFiles(for id: UUID) -> [(name: String, content: String)] {
listFiles(for: id).compactMap { url in
guard let content = try? String(contentsOf: url, encoding: .utf8) else { return nil }
return (url.lastPathComponent, content)
}
}
/// Returns file size in bytes, or nil if unavailable
func fileSize(at url: URL) -> Int? {
(try? url.resourceValues(forKeys: [.fileSizeKey]))?.fileSize
}
}