// // JarvisService.swift // oAI // // HTTP client for the Jarvis (oAI-Web) REST API. // Auth: Authorization: Bearer // // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Rune Olsen import Foundation import os final class JarvisService: Sendable { static let shared = JarvisService() private init() {} private let log = Logger(subsystem: "com.oai.oAI", category: "jarvis") private var baseURL: String { SettingsService.shared.jarvisURL } private var apiKey: String? { SettingsService.shared.jarvisAPIKey } // MARK: - Agents func listAgents() async throws -> [JarvisAgent] { try await get("/api/agents") } func createAgent(_ input: JarvisAgentInput) async throws -> JarvisAgent { try await post("/api/agents", body: input) } func updateAgent(id: String, _ input: JarvisAgentInput) async throws -> JarvisAgent { try await put("/api/agents/\(id)", body: input) } func deleteAgent(id: String) async throws { try await voidRequest("DELETE", path: "/api/agents/\(id)") } func toggleAgent(id: String) async throws -> JarvisAgent { try await post("/api/agents/\(id)/toggle", body: Empty()) } func runAgent(id: String) async throws { try await voidRequest("POST", path: "/api/agents/\(id)/run") } func stopAgent(id: String) async throws { try await voidRequest("POST", path: "/api/agents/\(id)/stop") } func agentRuns(id: String) async throws -> [JarvisAgentRun] { try await get("/api/agents/\(id)/runs") } // MARK: - Usage func usage() async throws -> [JarvisUsageStat] { let data = try await rawData("GET", path: "/api/usage") // API returns {summary:{...}, by_agent:[...], chat:{...}} if let w = try? JSONDecoder().decode(JarvisUsageResponse.self, from: data) { return w.byAgent ?? [] } // Fallback: plain array if let arr = try? JSONDecoder().decode([JarvisUsageStat].self, from: data) { return arr } return [] } func credits() async throws -> JarvisCreditsResponse { try await get("/api/usage/openrouter-credits") } // MARK: - Control func queueStatus() async throws -> JarvisQueueStatus { try await get("/api/status") } func pauseAll() async throws { try await voidRequest("POST", path: "/api/pause") } func resumeAll() async throws { try await voidRequest("POST", path: "/api/resume") } // MARK: - Connection test func testConnection() async -> Bool { guard !baseURL.isEmpty, let key = apiKey, !key.isEmpty else { return false } do { let _: [JarvisAgent] = try await get("/api/agents") return true } catch { log.warning("Jarvis connection test failed: \(error.localizedDescription)") return false } } // MARK: - HTTP core private func get(_ path: String) async throws -> T { let data = try await rawData("GET", path: path) return try JSONDecoder().decode(T.self, from: data) } private func post(_ path: String, body: B) async throws -> T { let data = try await rawData("POST", path: path, body: body) return try JSONDecoder().decode(T.self, from: data) } private func put(_ path: String, body: B) async throws -> T { let data = try await rawData("PUT", path: path, body: body) return try JSONDecoder().decode(T.self, from: data) } private func voidRequest(_ method: String, path: String) async throws { _ = try await rawData(method, path: path) } private func rawData(_ method: String, path: String, body: B? = nil as Empty?) async throws -> Data { guard let url = URL(string: baseURL.trimmingCharacters(in: .whitespaces) + path) else { throw JarvisError.invalidURL } guard let key = apiKey, !key.isEmpty else { throw JarvisError.noAPIKey } var req = URLRequest(url: url) req.httpMethod = method req.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization") req.setValue("application/json", forHTTPHeaderField: "Accept") req.timeoutInterval = 30 if let body { req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try JSONEncoder().encode(body) } let (data, response) = try await URLSession.shared.data(for: req) guard let http = response as? HTTPURLResponse else { throw JarvisError.invalidResponse } guard (200..<300).contains(http.statusCode) else { let msg = (try? JSONDecoder().decode(JarvisErrBody.self, from: data))?.detail ?? "HTTP \(http.statusCode)" log.error("Jarvis \(method) \(path) → \(http.statusCode): \(msg)") throw JarvisError.serverError(http.statusCode, msg) } return data } } private struct Empty: Codable {} private struct JarvisErrBody: Decodable { let detail: String? }