mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
19b4ba9995
Brings the iOS companion branch current with main's v2.2.0, v2.2.1,
and v2.3.0 landings — templates + configuration + catalog (v2.2),
projects folder hierarchy + per-project Sessions sidecar + AGENTS.md
context block + Tool Gateway + Nous Portal OAuth + hermes dashboard
webview (v2.3), and credential-pool OAuth expiry + Nous agent-key
rotation (post-v2.3).
Resolutions:
- ScarfCore Models (HermesConfig, ProjectDashboard, HermesPathSet) —
forward-ported Tool Gateway's platformToolsets, project-registry v2
folder/archived fields, and sessionProjectMap path into the moved
ScarfCore copies. Deleted the old Mac-target paths.
- ScarfCore ModelCatalogService — merged main's overlay-only provider
support (Nous Portal + OpenAI Codex + Qwen OAuth + …) so iOS and
macOS pickers see the same provider list. Widened HermesProviderInfo
/ HermesProviderOverlay APIs to public.
- ScarfCore ProjectsViewModel — layered main's v2.3 registry verbs
(moveProject / renameProject / archive / unarchive / folders) onto
the M0d-extracted VM, keeping public surface for the Mac target.
- ScarfCore ConnectionStatusViewModel / RichChatViewModel — widened
`private(set)` to `public private(set)` so Mac views can read
status, lastSuccess, acp*Tokens, originSessionId, acpCommands,
quickCommands.
- ScarfCore HermesConfig+YAML — added platform_toolsets parsing to
the iOS YAML path so config.yaml round-trips the same as macOS.
- RichChatViewModel quick-commands — inlined the Mac-target's
QuickCommandsViewModel.loadQuickCommands into ScarfCore using the
existing HermesYAML parser, removing the cross-module dependency.
- HealthViewModel — took main's Tool Gateway + hermes-dashboard
webview sections wholesale; file stays macOS-only.
- ChatView auto-merge — confirmed resume-session fix (5ae8db2) is
present; made the PendingPermission.id extension public to satisfy
Identifiable conformance across module boundary.
- ProjectSessionsViewModel — moved back to the Mac target since it
depends on SessionAttributionService (also Mac-target). Defer the
iOS SFTP parity of attribution to M7.
- LocalTransport.runProcess + SSHTransport.runLocal — wrapped the
Process body in `#if !os(iOS)` with an explicit throw on iOS so
ScarfCore compiles under the iOS SDK. iOS uses
CitadelServerTransport (ScarfIOS) as the real implementation.
- CitadelServerTransport — updated `sftp.remove(atPath:)` to
`sftp.remove(at:)` for the current Citadel API shape.
Cross-module imports: added `import ScarfCore` to 25 Mac-target files
that consumed ScarfCore types (13 v2.3 additions + 12 post-merge
errors caught by MemberImportVisibility: Settings tabs, SidebarView,
MCPServerEditorView, TemplateExportSheet, tests).
Version lockstep: bumped `scarf mobile` target to
MARKETING_VERSION=2.3.0, CURRENT_PROJECT_VERSION=25 to match main.
Builds green for both schemes:
- swift build (ScarfCore standalone)
- xcodebuild scarf -destination platform=macOS
- xcodebuild 'scarf mobile' -destination generic/platform=iOS
Deferred to M7 (iOS SFTP parity):
- NousSubscriptionService auth.json reader
- ProjectAgentContextService AGENTS.md write-before-chat
- SessionAttributionService session_project_map.json read/watch
All currently Mac-target-gated; iOS still builds without them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
3.7 KiB
Swift
87 lines
3.7 KiB
Swift
import Foundation
|
|
import os
|
|
import ScarfCore
|
|
|
|
/// Snapshot of the user's Nous Portal subscription state, derived from the
|
|
/// `providers.nous` entry in `~/.hermes/auth.json`. Read-only — Scarf never
|
|
/// writes the subscription record; `hermes model` + `hermes auth` own that
|
|
/// path.
|
|
struct NousSubscriptionState: Sendable, Hashable {
|
|
/// True when `providers.nous` exists and has a usable access token.
|
|
/// Mirrors the `nous_auth_present` field on
|
|
/// `NousSubscriptionFeatures` in `hermes_cli/nous_subscription.py`.
|
|
let present: Bool
|
|
/// True when the user's **active provider** is `nous`, i.e. they've not
|
|
/// just authed but selected it as the primary model provider. The Tool
|
|
/// Gateway only routes tools when this is true — auth alone isn't enough.
|
|
let providerIsNous: Bool
|
|
/// Last update time for the auth record, if known. Useful in the Health
|
|
/// view to tell the user when their subscription state was last refreshed.
|
|
let updatedAt: Date?
|
|
|
|
static let absent = NousSubscriptionState(present: false, providerIsNous: false, updatedAt: nil)
|
|
|
|
/// Overall subscription active for Tool Gateway routing. Both halves have
|
|
/// to line up: auth record present *and* `nous` is the active provider.
|
|
/// Mirrors `NousSubscriptionFeatures.subscribed` on the Python side.
|
|
var subscribed: Bool { present && providerIsNous }
|
|
}
|
|
|
|
/// Reads `auth.json` to detect Nous Portal subscription state. Delegates file
|
|
/// I/O to the active `ServerTransport`, so remote installations work the same
|
|
/// as local ones.
|
|
///
|
|
/// The auth-record shape is defined by hermes-agent and is load-bearing. This
|
|
/// service parses a small, stable subset and tolerates anything new Hermes
|
|
/// adds — we only rely on `providers.nous` being a dict with `access_token`
|
|
/// and `active_provider` being either `"nous"` or not.
|
|
struct NousSubscriptionService: Sendable {
|
|
private let logger = Logger(subsystem: "com.scarf", category: "NousSubscriptionService")
|
|
let authJSONPath: String
|
|
let transport: any ServerTransport
|
|
|
|
nonisolated init(context: ServerContext = .local) {
|
|
self.authJSONPath = context.paths.authJSON
|
|
self.transport = context.makeTransport()
|
|
}
|
|
|
|
/// Escape hatch for tests — point at a fixture `auth.json` without
|
|
/// constructing a full `ServerContext`. Uses `LocalTransport` so the
|
|
/// fixture must live on the local filesystem.
|
|
init(path: String) {
|
|
self.authJSONPath = path
|
|
self.transport = LocalTransport()
|
|
}
|
|
|
|
/// Load the current subscription state. Returns ``NousSubscriptionState/absent``
|
|
/// on any read or parse failure — callers treat "absent" and "can't
|
|
/// read" the same in UI (show a "not subscribed" CTA).
|
|
nonisolated func loadState() -> NousSubscriptionState {
|
|
guard let data = try? transport.readFile(authJSONPath) else {
|
|
return .absent
|
|
}
|
|
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
logger.warning("auth.json is not a JSON object; assuming no Nous subscription")
|
|
return .absent
|
|
}
|
|
let providers = root["providers"] as? [String: Any] ?? [:]
|
|
let nous = providers["nous"] as? [String: Any]
|
|
let token = nous?["access_token"] as? String
|
|
let present = (token?.isEmpty == false)
|
|
|
|
let activeProvider = root["active_provider"] as? String
|
|
let providerIsNous = (activeProvider == "nous")
|
|
|
|
let updatedAt: Date? = {
|
|
guard let raw = root["updated_at"] as? String else { return nil }
|
|
return ISO8601DateFormatter().date(from: raw)
|
|
}()
|
|
|
|
return NousSubscriptionState(
|
|
present: present,
|
|
providerIsNous: providerIsNous,
|
|
updatedAt: updatedAt
|
|
)
|
|
}
|
|
}
|