docs: phase 2 — fill all 26 stubs with real content

Alan Wizemann
2026-04-20 15:29:08 -07:00
parent 51dd4bb604
commit 7aeb1ff17f
26 changed files with 1585 additions and 59 deletions
+95 -2
@@ -1,6 +1,99 @@
# ACP Subprocess # ACP Subprocess
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). ACP — Agent Client Protocol — is Hermes's chat protocol: JSON-RPC 2.0 over stdio. Scarf's Rich Chat surface speaks ACP end-to-end. There is no SQLite polling involved in chat; tokens, thoughts, tool calls, and permission prompts all stream live from the subprocess.
## Subprocess construction
[`ACPClient`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/ACPClient.swift) is an `actor` that owns the subprocess and exposes an `AsyncStream<ACPEvent>`.
The `Process` is created via `transport.makeProcess(executable: "hermes", args: ["acp"])`:
- **Local:** spawns `hermes acp` with `HermesFileService.enrichedEnvironment()` so MCP servers and shell tools find brew/nvm/asdf binaries on `PATH`. `TERM` is removed so terminal escapes don't pollute JSON-RPC.
- **Remote:** the transport returns `/usr/bin/ssh -T <opts> host -- hermes acp`. `SSH_AUTH_SOCK` is inherited so the GUI-launched Scarf reaches the user's ssh-agent. `TERM` is removed.
`-T` (no PTY) is critical — without it stdin/stdout would be PTY-cooked and the JSON-RPC framing would break.
## Lifecycle
| Phase | What happens |
|---|---|
| `start()` | Creates the event stream first, builds and configures the Process, attaches pipes, installs the termination handler, calls `proc.run()`, starts read loops for stdout/stderr, sends `initialize`, starts a 30-second keepalive ping (`{"jsonrpc":"2.0","method":"$/ping"}`). |
| `newSession(cwd:)` / `loadSession(cwd:sessionId:)` / `resumeSession(cwd:sessionId:)` | Three modes: fresh, load existing, resume after disconnect. Each sends a `session/new`/`session/load`/`session/resume` RPC; updates `currentSessionId`. |
| `sendPrompt(sessionId:text:)` | Sends `session/prompt` with the user text; returns `ACPPromptResult` with token usage and stop reason. **No timeout** — streaming may run for minutes. Tokens, thoughts, tool calls, and permission requests arrive as events on the stream while this awaits. |
| `cancel(sessionId:)` | Sends `session/cancel` to interrupt an in-flight prompt. |
| `respondToPermission(requestId:optionId:)` | Sends a JSON-RPC response to an incoming `session/request_permission` request. |
| `stop()` | Cancels background tasks, finishes the event continuation, closes stdin (subprocess sees EOF), sends SIGINT, watchdogs to SIGTERM after 2 seconds, closes pipes. |
## Event stream
Consumers iterate `for await event in client.events`:
```swift
enum ACPEvent: Sendable {
case messageChunk(sessionId, text) // assistant token chunk
case thoughtChunk(sessionId, text) // reasoning/thinking token chunk
case toolCallStart(sessionId, call) // tool invocation began
case toolCallUpdate(sessionId, update) // tool invocation finished/updated
case permissionRequest(sessionId, requestId, request) // user approval needed
case promptComplete(sessionId, response) // session/prompt resolved
case availableCommands(sessionId, commands) // /commands the agent advertises
case connectionLost(reason)
case unknown(sessionId, type)
}
```
Events are extracted from incoming JSON-RPC notifications matching `method: "session/update"`. The `sessionUpdate` discriminator inside `params` selects the case (`agent_message_chunk`, `agent_thought_chunk`, `tool_call`, `tool_call_update`, etc.).
## Permission requests (bidirectional)
Most chat traffic is agent → client. Permission requests reverse direction — the agent sends an incoming JSON-RPC **request** with `method: "session/request_permission"`:
```json
{
"jsonrpc": "2.0",
"id": 42,
"method": "session/request_permission",
"params": {
"sessionId": "...",
"toolCall": { ... },
"options": [
{"optionId": "allow", "name": "Allow"},
{"optionId": "deny", "name": "Deny"}
]
}
}
```
The client emits `ACPEvent.permissionRequest`. The UI (Rich Chat) shows a sheet; once the user clicks an option, `respondToPermission(requestId: 42, optionId: "allow")` sends back:
```json
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"outcome": {"kind": "allowed", "optionId": "allow"}
}
}
```
## Internals
- **Read loop** runs detached: `availableData` → buffer → split on `\n` → JSON-decode each line → `handleMessage`.
- **Stderr loop** captures the subprocess's stderr into a 50-line ring buffer for attaching to user-visible errors.
- **Pending requests dict** maps JSON-RPC `id``CheckedContinuation<AnyCodable?, Error>`. Responses resume the matching continuation; the read loop dispatches them by `id`.
- **30-second control-message timeout** fires for `initialize`/`session/new`/etc. There is no timeout on `session/prompt` — that one streams for as long as the model takes.
- **`safeWrite(fd:data:)`** handles partial writes and EPIPE; used for both prompt sends and keepalive pings.
- **Disconnect cleanup** is single-pathed via `performDisconnectCleanup(reason)`. Three callers: stdout EOF (`handleReadLoopEnded`), process termination (`handleTermination`), write failure (`handleWriteFailed`). All three resume pending requests with `processTerminated` and finish the event continuation.
## Error hints
Raw error messages are noisy. `ACPErrorHint` (in the same file) pattern-matches across the error message and the stderr ring buffer to attach actionable hints:
| Pattern matched | Hint surfaced |
|---|---|
| "No credentials found" / `ANTHROPIC_API_KEY` | "Set `ANTHROPIC_API_KEY` in `~/.hermes/.env`" |
| "No such file or directory" + binary name | "Binary not on `PATH`; check `~/.zprofile` exports" |
| "Rate limit" / 429 | "AI provider rate-limited; try again later" |
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+146 -2
@@ -1,6 +1,150 @@
# Adding a Feature Module # Adding a Feature Module
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). The MVVM-F recipe. Adding a feature touches 4 existing files and creates 2 new ones.
## Directory shape
```
scarf/scarf/scarf/Features/MyFeature/
Views/
MyFeatureView.swift
ViewModels/
MyFeatureViewModel.swift
```
Both subdirectories are conventional — even features that only have one view of each follow this shape so file discovery is consistent.
## Step 1: Create the ViewModel
```swift
import Observation
@Observable
final class MyFeatureViewModel {
let context: ServerContext
private let fileService: HermesFileService
init(context: ServerContext) {
self.context = context
self.fileService = HermesFileService(context: context)
}
var items: [MyModel] = []
var loadError: String?
func load() async {
do {
items = try await fetchItems()
loadError = nil
} catch {
loadError = error.localizedDescription
}
}
}
```
Conventions:
- Always take `ServerContext` in `init` so the feature works against any window's bound server.
- Construct any services inside `init`; don't hand them in.
- Use `@Observable` (Swift macro), not `ObservableObject`.
- Public state is `var` properties; mutate them on `MainActor`. Async work runs `nonisolated` and assigns the final value back on the main actor.
## Step 2: Create the View
```swift
struct MyFeatureView: View {
@State private var viewModel: MyFeatureViewModel
@Environment(AppCoordinator.self) private var coordinator
@Environment(HermesFileWatcher.self) private var fileWatcher
init(context: ServerContext) {
_viewModel = State(initialValue: MyFeatureViewModel(context: context))
}
var body: some View {
List(viewModel.items) { item in /* */ }
.navigationTitle("My Feature")
.task { await viewModel.load() }
.task(id: fileWatcher.lastChangeDate) {
// Re-load when ~/.hermes/ changes
await viewModel.load()
}
}
}
```
Conventions:
- View takes `ServerContext` in its `init`; it's the only initializer parameter.
- `@State private var viewModel: MyFeatureViewModel``@State` is the right wrapper for `@Observable` classes inside views.
- Read coordinator and watcher from `@Environment`.
- Use `.task(id:)` for reactive reloads — make sure you include every dependency in the id, or changes to a missing one won't trigger reload.
## Step 3: Add the SidebarSection case
In [`Navigation/AppCoordinator.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/AppCoordinator.swift):
```swift
enum SidebarSection: String, CaseIterable, Identifiable {
// existing cases
case myFeature = "My Feature"
var icon: String {
switch self {
// existing icons
case .myFeature: return "star.fill" // pick an SF Symbol
}
}
}
```
## Step 4: Register in SidebarView
In [`Navigation/SidebarView.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/SidebarView.swift), add the case to the right `Section`:
```swift
Section("Interact") {
ForEach([SidebarSection.chat, .memory, .skills, .myFeature]) { section in
Label(section.rawValue, systemImage: section.icon).tag(section)
}
}
```
Pick the section thematically — Monitor for views, Interact for talking-to-Hermes, Configure for setup, Manage for operational.
## Step 5: Wire routing
In `ContentView.swift`'s `detailView` switch:
```swift
switch coordinator.selectedSection {
// existing cases
case .myFeature: MyFeatureView(context: serverContext)
}
```
## Step 6: (If your feature uses a new service)
If you needed a new service to back this feature, add it under `Core/Services/` and inject any shared instance in `ContextBoundRoot` via `.environment(...)`. See [Adding a Service](Adding-a-Service).
## Cross-feature rules
The hard rules ([CLAUDE.md](https://github.com/awizemann/scarf/blob/main/CLAUDE.md)):
- **Features never import sibling features.** If `MyFeature` needs data another feature also uses, the data lives in a service, not in that other feature.
- **Cross-feature navigation goes through `AppCoordinator`.** Set `coordinator.selectedSection = .otherFeature` and (if needed) `coordinator.selectedSessionId = ...`.
## Files touched
- ✏️ `Navigation/AppCoordinator.swift` — 1 enum case, 1 icon line.
- ✏️ `Navigation/SidebarView.swift` — add to the right `Section`.
- ✏️ `ContentView.swift` — 1 switch case.
- ✏️ `scarfApp.swift` — only if you needed to inject a new shared service.
-`Features/MyFeature/Views/MyFeatureView.swift` — new.
-`Features/MyFeature/ViewModels/MyFeatureViewModel.swift` — new.
Total: ~5-10 lines across 4 existing files, plus 2 new files.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+108 -2
@@ -1,6 +1,112 @@
# Adding a Service # Adding a Service
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Services live under `scarf/scarf/scarf/Core/Services/`. They mediate between Hermes (filesystem, SQLite, subprocess) and feature ViewModels. The recipe is short.
## Pick the isolation
| Pattern | When | Examples in repo |
|---|---|---|
| `actor` | The service owns mutable state across calls (a subprocess, file handles, an open SQLite connection, a snapshot dedup table). | [`ACPClient`](Core-Services), [`HermesDataService`](Core-Services), [`HermesLogService`](Core-Services). |
| `Sendable struct` | The service is stateless — every call re-reads through the transport. | [`HermesFileService`](Core-Services), [`HermesEnvService`](Core-Services), [`ModelCatalogService`](Core-Services). |
| `@MainActor @Observable` | Rare — only when the service IS UI-observable state (Sparkle wrapper). | [`UpdaterService`](Core-Services). |
If you're not sure, start with `Sendable struct`. Promote to `actor` only when you find yourself wanting to cache mutable state across calls.
## Skeleton: stateless struct
```swift
import Foundation
struct MyHermesService: Sendable {
let context: ServerContext
private var transport: any ServerTransport { context.transport }
func loadSomething() async throws -> SomethingType {
let data = try await transport.readFile(context.paths.somethingFile)
return try JSONDecoder().decode(SomethingType.self, from: data)
}
func saveSomething(_ value: SomethingType) async throws {
let data = try JSONEncoder().encode(value)
try await transport.writeFile(context.paths.somethingFile, data: data)
}
}
```
## Skeleton: actor for stateful work
```swift
actor MyStatefulService {
let context: ServerContext
private var cache: [String: Value] = [:]
init(context: ServerContext) {
self.context = context
}
func get(_ key: String) async throws -> Value {
if let cached = cache[key] { return cached }
let value = try await fetchFromHermes(key: key)
cache[key] = value
return value
}
func invalidate() {
cache.removeAll()
}
private func fetchFromHermes(key: String) async throws -> Value {
// Use context.transport for I/O.
}
}
```
## Conventions
- **Take `ServerContext` in `init`.** Never hardcode `ServerContext.local` — services must work against any window's bound server.
- **Route I/O through `context.transport` or the `context.read*/write*/runHermes` helpers.** Never use `FileManager`, `Process`, or `NSWorkspace.open` directly for Hermes paths — those break on remote (and break the rule from the project's [feedback memory](Wiki-Maintenance)).
- **Surface errors as `throws` or `Result`.** Don't swallow them; the UI knows what to do with them.
- **Don't log to `print`** — use `os.Logger` (`logger.error()` for unexpected, `logger.warning()` for expected).
- **Don't do synchronous file I/O on `@MainActor`.** Either dispatch via `Task.detached { }.value`, or expose async methods.
## Wiring the service into a feature
Two patterns:
**Per-ViewModel** (most common for stateless services):
```swift
@Observable
final class MyFeatureViewModel {
private let service: MyHermesService
init(context: ServerContext) {
self.service = MyHermesService(context: context)
}
}
```
**Shared via Environment** (for stateful services that multiple features want to share):
In `scarfApp.swift`'s `ContextBoundRoot`:
```swift
@State private var fileWatcher: HermesFileWatcher
ContentView()
.environment(fileWatcher)
```
Then in any view:
```swift
@Environment(HermesFileWatcher.self) private var fileWatcher
```
Use Environment for things every window has exactly one of — file watcher, server registry, updater service. Use per-ViewModel construction for everything else.
## Tests
Service code that uses transport is testable with a mock transport. See [Testing](Testing) — what exists today is minimal, but the `ServerTransport` protocol is the obvious extension point for fakes.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+97 -2
@@ -1,6 +1,101 @@
# Build and Run # Build and Run
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Scarf is a single Xcode target with one SPM dependency (Sparkle). No CocoaPods, no Carthage, no submodules.
## Prerequisites
- **macOS 14.6+ (Sonoma)** on the dev machine.
- **Xcode 16.0+**.
- **Hermes** at `~/.hermes/` (so the local server window has something to point at — see [First Run](First-Run)).
## Open in Xcode
```bash
git clone https://github.com/awizemann/scarf.git
cd scarf
open scarf/scarf.xcodeproj
```
The project uses `PBXFileSystemSynchronizedRootGroup` — Xcode auto-discovers any new file you drop into the source tree. You don't need to manually add files to the target.
Build with ⌘B; run with ⌘R.
## Build from the command line
Debug build:
```bash
xcodebuild -project scarf/scarf.xcodeproj -scheme scarf -configuration Debug build
```
Universal release build (matches what the release script produces):
```bash
xcodebuild -project scarf/scarf.xcodeproj -scheme scarf \
-configuration Release \
-arch arm64 -arch x86_64 ONLY_ACTIVE_ARCH=NO build
```
## Project layout
```
scarf/ repo root
CLAUDE.md project instructions for Claude Code
CONTRIBUTING.md
README.md
releases/v<ver>/ per-version notes + appcast entry
scripts/
release.sh full release pipeline
wiki.sh this wiki helper
ExportOptions.plist
scarf/ Xcode project root
scarf.xcodeproj
docs/ internal dev notes (PRD, Discovery, ARCH)
standards/ read-only reference standards
scarf/ APP TARGET — start here
scarfApp.swift @main App
ContentView.swift window root
Core/
Services/ 9 services
Models/ 13+ models
Transport/ 4 transport files
Persistence/ ServerRegistry
Utilities/ markdown helpers
Features/ 25 feature modules
Navigation/ AppCoordinator + SidebarView
Assets.xcassets
Info.plist
scarf.entitlements
scarfTests/
scarfUITests/
```
## Swift 6 concurrency
The project compiles with strict concurrency on. Two non-negotiables ([detail in CLAUDE.md](https://github.com/awizemann/scarf/blob/main/CLAUDE.md)):
- `@MainActor` is the default isolation. Services use `nonisolated` async methods and route results back to MainActor for UI updates.
- All closures captured by `Task`, `Task.detached`, or `withCheckedThrowingContinuation` must be `@Sendable`. Don't capture non-Sendable types across concurrency boundaries.
## What to look at first
1. [`scarfApp.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/scarfApp.swift) — App entry point, multi-window setup, `ContextBoundRoot` injecting the `ServerContext` and friends.
2. [`Navigation/AppCoordinator.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/AppCoordinator.swift) — single source of truth for navigation.
3. [`Core/Models/ServerContext.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/ServerContext.swift) — the unified handle to a Hermes install.
4. [`Core/Transport/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/Core/Transport) — local vs. SSH abstraction.
5. Any feature module under [`Features/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/Features) for the MVVM-F shape.
## Running tests
```bash
xcodebuild test -project scarf/scarf.xcodeproj -scheme scarf
```
See [Testing](Testing) for what is and isn't tested.
## Releasing
Don't run `xcodebuild archive` / `notarytool` / `gh release create` by hand — use the release script. See [Release Process](Release-Process).
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+51 -2
@@ -1,6 +1,55 @@
# Chat # Chat
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Two modes share the same sidebar item: **Rich Chat** (real-time ACP streaming with iMessage-style bubbles) and **Terminal** (the `hermes chat` CLI rendered with full ANSI color in an embedded terminal). Switch between them in the chat toolbar.
## Rich Chat
Streams tokens, thoughts, and tool calls live via the [ACP subprocess](ACP-Subprocess). No DB polling — when Hermes emits a message chunk, you see it immediately.
**What it renders:**
- iMessage-style bubbles, user vs. assistant.
- Markdown content (links, lists, code blocks, tables).
- **Reasoning / thinking** chunks in a collapsible block above the answer.
- **Tool calls** as inline cards with the function name, argument summary, and (when complete) the result.
- **Permission requests** as modal sheets — Hermes asks before executing risky tools; you choose Allow / Deny.
- **`/compress`** — when Hermes advertises the command via `availableCommands`, a one-click focus sheet appears in the toolbar.
**Session lifecycle:**
- Each new conversation creates a session via `session/new` (cwd = your local working directory if you set one).
- Resume an old conversation: pick from the session picker; Scarf calls `session/load` or `session/resume` depending on the state.
- Auto-reconnect — if the subprocess dies, Scarf attempts a `session/resume` to pick up where it left off.
**Voice mode controls:** PTT (push-to-talk), TTS playback, STT transcription preferences live in **Settings → Voice**. The chat toolbar exposes the basic toggles.
## Terminal mode
The full `hermes chat` CLI rendered in an embedded SwiftTerm terminal:
- ANSI color and Rich formatting work as in your shell.
- WhatsApp / Signal pairing flows render their QR codes here for scanning.
- Useful when you need a feature only the CLI exposes (e.g. obscure flags, daemon management).
## Cancellation
The Stop button sends `session/cancel` over the JSON-RPC channel. The model stops generating; any in-flight tool call completes. You can immediately send another prompt without restarting the session.
## Multi-server chat
Each window is bound to one server, so chat in window A talks to local Hermes while window B talks to a remote one. Sessions don't cross windows — they live on the server they were created on.
## Troubleshooting
- **"Spinning forever, no response"** — check the [Logs](Gateway-Cron-Health-Logs) view for ACP errors. Common causes: missing `ANTHROPIC_API_KEY` (Scarf attaches a hint), rate limit, or `hermes` binary not on `$PATH`.
- **Connection lost** — Rich Chat surfaces this banner; click Reconnect to call `session/resume`.
- **Permission sheet doesn't appear** — make sure Hermes's `approval_mode` in `config.yaml` is set so it asks (not auto-approve).
## Related pages
- [ACP Subprocess](ACP-Subprocess) for the protocol internals.
- [Memory & Skills](Memory-and-Skills) for editing what Hermes knows about you.
- [Settings — Voice tab](Gateway-Cron-Health-Logs) for TTS/STT configuration (Settings is documented there).
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+56 -2
@@ -1,6 +1,60 @@
# Contributing # Contributing
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Thanks for your interest in contributing to Scarf. The canonical contributor guide lives in [`CONTRIBUTING.md`](https://github.com/awizemann/scarf/blob/main/CONTRIBUTING.md) in the repo. This page is a public-facing summary plus a few wiki-specific notes.
## Quick start
1. Fork and clone <https://github.com/awizemann/scarf>.
2. Open `scarf/scarf.xcodeproj` in Xcode 16+.
3. Build and run (requires macOS 14.6+ and Hermes installed at `~/.hermes/`).
4. Read [Build & Run](Build-and-Run) for the codebase tour and [Architecture Overview](Architecture-Overview) for the layering.
## Code conventions
The full list is in [`CONTRIBUTING.md`](https://github.com/awizemann/scarf/blob/main/CONTRIBUTING.md). The non-negotiables:
- **MVVM-F.** Features never import sibling features. Cross-feature concerns go through services or `AppCoordinator`.
- **No commented-out code, no TODOs, no deferred functionality** in PRs.
- **Zero warnings** on build.
- **Read-only DB access.** Never write to `~/.hermes/state.db`.
- **Swift 6 strict concurrency.** `@MainActor` default isolation; `nonisolated` for service methods.
- **Conventional commits.** `feat:`, `fix:`, `chore:`, `refactor:`, `docs:`, etc.
## What's good to work on
- Anything in the [Roadmap](Roadmap).
- Test coverage — see [Testing](Testing) for the highest-value gaps.
- Wiki content — every stub on the wiki is a pull request opportunity. See [Wiki Maintenance](Wiki-Maintenance) for the workflow.
- Bug reports with reproducible steps.
## Pull request flow
1. Open an issue first describing the change. This avoids rework if the maintainer has constraints in mind.
2. One feature or fix per PR — keeps reviews tight.
3. Include a clear description of what changed and why.
4. Ensure `xcodebuild -project scarf/scarf.xcodeproj -scheme scarf build` succeeds with zero warnings.
## Reporting issues
Open an issue at <https://github.com/awizemann/scarf/issues> with:
- What you expected to happen.
- What actually happened.
- macOS version and Hermes version.
- Steps to reproduce.
- The relevant log snippet from `~/.hermes/logs/errors.log` if applicable (redact secrets first).
## Documentation contributions
Two paths:
- **Small wiki edits** (typos, clarifications) — click **Edit** on any wiki page in the GitHub UI. Editing requires push access to the main repo.
- **Larger wiki changes** — clone the wiki directly (`git clone git@github.com:awizemann/scarf.wiki.git`) or open an issue describing what needs adding. See [Wiki Maintenance](Wiki-Maintenance) for the full workflow including the secret-scan policy.
- **Internal dev docs** (PRD, Hermes API discovery, raw architecture notes) live in `scarf/docs/` in the main repo and follow the normal PR flow.
## Code of conduct
Be kind, be specific, assume good faith. Disagreements about technical direction are welcome; personal attacks aren't.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+27 -2
@@ -1,6 +1,31 @@
# Core Services # Core Services
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). The 9 services under [`scarf/scarf/scarf/Core/Services/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/Core/Services) are the data-and-side-effects layer between Hermes and the SwiftUI features. Each takes a `ServerContext` (local or SSH) so the same service code works against a local install or a remote one.
| Service | Isolation | Lines | Purpose |
|---|---|---|---|
| [`ACPClient`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/ACPClient.swift) | `actor` | ~605 | Spawns `hermes acp` subprocess; JSON-RPC over stdio; async event stream for chat. |
| [`HermesDataService`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/HermesDataService.swift) | `actor` | ~658 | Read-only SQLite queries against `state.db`; pulls atomic snapshots for remote; dedupes concurrent snapshot calls via a nested `SnapshotCoordinator` actor. |
| [`HermesEnvService`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/HermesEnvService.swift) | `Sendable struct` | ~217 | Non-destructive `~/.hermes/.env` I/O — preserves comments and blanks; `unset` comments out instead of deleting. |
| [`HermesFileService`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/HermesFileService.swift) | `Sendable struct` | ~600 | Parses `config.yaml` into typed nested structs; resolves `hermes` binary; enriches `$PATH` for spawned tools (brew/nvm/asdf). |
| [`HermesFileWatcher`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/HermesFileWatcher.swift) | `@Observable` | ~122 | Local: FSEvents via `DispatchSourceFileSystemObject`. Remote: mtime polling over the SSH ControlMaster. Updates `lastChangeDate`; views observe and refresh. |
| [`HermesLogService`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/HermesLogService.swift) | `actor` | ~173 | Tails `agent.log` / `errors.log` / `gateway.log`. Local: `FileHandle`. Remote: `ssh host tail -F` with partial-line buffering. |
| [`ModelCatalogService`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/ModelCatalogService.swift) | `Sendable struct` | ~210 | Reads `~/.hermes/models_dev_cache.json` (~1500 models across ~110 providers); helpers for cost and context-window display. |
| [`ProjectDashboardService`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/ProjectDashboardService.swift) | `Sendable struct` | ~71 | Loads/saves the project registry and per-project `.scarf/dashboard.json`. |
| [`UpdaterService`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/UpdaterService.swift) | `@MainActor @Observable` | ~41 | Thin Sparkle wrapper exposing the auto-check toggle, last-check date, and a "check now" trigger. |
## Patterns shared across the layer
- **`ServerContext` parameterizes all I/O.** Services receive the context at init; routing local vs. SSH happens through `context.transport`. See [Transport Layer](Transport-Layer).
- **Stateful services are `actor`s.** ACPClient, HermesDataService, and HermesLogService own resources (subprocesses, file handles, SQLite connections) that need serialized access; they expose async APIs.
- **Stateless services are `Sendable struct`s.** HermesEnvService, HermesFileService, ModelCatalogService, and ProjectDashboardService have no instance state worth coordinating; each call re-reads through the transport.
- **Schema tolerance.** `HermesDataService` checks for v0.7+ columns (`reasoning_tokens`, `actual_cost_usd`, `cost_status`, `billing_provider`) and degrades gracefully on older databases.
- **Snapshot dedup.** `SnapshotCoordinator` (nested in `HermesDataService`) ensures concurrent callers from Dashboard + Sessions + Activity await the same in-flight `sqlite3 .backup` rather than each spawning a fresh one.
- **Error hints over raw stderr.** `ACPClient` keeps a 50-line stderr ring buffer and pattern-matches into `ACPErrorHint` for user-friendly messages (missing `ANTHROPIC_API_KEY`, binary not on `PATH`, rate-limited).
## Adding a service
See [Adding a Service](Adding-a-Service) for the recipe. Short version: take `ServerContext` in `init`, decide isolation (`actor` for stateful, `struct` for stateless), expose async public methods, route I/O through `context.transport`.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+41 -2
@@ -1,6 +1,45 @@
# Dashboard # Dashboard
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). The Dashboard is the default landing view of every window. It answers four questions at a glance: **Is Hermes running?** **What's it cost me?** **What did it just do?** **Is anything broken?**
## Layout
A scrolling stack of cards, refreshed automatically when `~/.hermes/state.db`, `config.yaml`, or the logs change ([HermesFileWatcher](Core-Services) handles the reload).
| Card | Source | What it shows |
|---|---|---|
| **Hermes Process** | `pgrep` (local) or `ssh host pgrep` (remote) | Running / stopped, PID, uptime. Start/stop/restart buttons. |
| **Token Usage** | `state.db` aggregations across all sessions | Input + output + cache + reasoning tokens; period selector (7/30/90 days / all). |
| **Cost Tracking** | Per-session `actual_cost_usd` (v0.7+) or `estimated_cost_usd` | Total spend in the period, broken down by model when available. |
| **Recent Sessions** | `sessions` table, ordered by `started_at DESC` | Last N sessions with title, source platform, message count, duration. Click to drill into [Sessions](Insights-and-Activity). |
| **Active Platforms** | Parsed from `config.yaml` | Which messaging platforms are configured and their connection status. |
| **Status** | `lastOpenError` from `HermesDataService` | Surfaced when the database can't be read (missing `sqlite3` on remote, permission denied, etc.). Links to [Health](Gateway-Cron-Health-Logs) for diagnostics. |
## Live refresh
Local windows watch `~/.hermes/` with FSEvents (`DispatchSourceFileSystemObject`). Remote windows poll mtimes every 3 seconds over the SSH ControlMaster. Either way, the Dashboard updates without needing a manual refresh — open it on a second monitor and watch sessions tick by as Hermes works.
## Status pill in the toolbar
Every Scarf window has a connection pill in the toolbar showing the bound server and its state:
- **Local** — green dot, "Local" label.
- **Remote (healthy)** — green dot, server name.
- **Remote (degraded)** — yellow dot, "Can't read Hermes state" — opens a diagnostics sheet that runs 14 checks in one SSH session (connectivity, `sqlite3` presence, read access on `config.yaml`/`state.db`, effective non-login `$PATH`).
- **Remote (failed)** — red dot, error message.
## When something looks wrong
- **"Hermes not running"** — start Hermes from a terminal, or click the Start button.
- **Empty cards** — `~/.hermes/state.db` is missing or unreadable. See [First Run](First-Run).
- **Token/cost are zero but you've used Hermes** — the schema may predate v0.7. Update Hermes; Scarf detects the new columns automatically.
- **Yellow "Can't read Hermes state" pill on remote** — open Manage Servers → Run Diagnostics. Each failed check explains why and how to fix.
## Related pages
- [Insights & Activity](Insights-and-Activity) for deeper analytics.
- [Chat](Chat) for talking to the running Hermes.
- [Servers & Remote](Servers-and-Remote) for adding remote hosts and the diagnostics flow.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+60 -2
@@ -1,6 +1,64 @@
# Data Model # Data Model
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Plain Codable structs under [`scarf/scarf/scarf/Core/Models/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/Core/Models). These are the in-memory representation of Hermes state — sessions, messages, config, cron jobs, MCP servers, project dashboards. None of them own I/O; services read from `~/.hermes/` (via [transport](Transport-Layer)) and decode into these types.
## Sessions and messages
| Type | Conformances | Notable fields | Notes |
|---|---|---|---|
| `HermesSession` | `Identifiable, Sendable` | `id, source, userId, model, title, parentSessionId, startedAt, endedAt, endReason, messageCount, toolCallCount, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, estimatedCostUSD, actualCostUSD, costStatus, billingProvider` | `isSubagent` = `parentSessionId != nil`; `displayCostUSD` prefers `actualCostUSD`; `sourceIcon` calls `KnownPlatforms.icon(for:source)`. |
| `HermesMessage` | `Identifiable, Sendable` | `id, sessionId, role, content, toolCallId, timestamp, tokenCount, finishReason, toolCalls, toolName, reasoning` | `isUser/isAssistant/isToolResult` helpers; `hasReasoning` for v0.7+ reasoning support. |
| `HermesToolCall` (nested in `HermesMessage`) | custom Codable | `callId, functionName, arguments` | `toolKind` categorizes to `.read/.edit/.execute/.fetch/.browser/.other`; `argumentsSummary` extracts command/path/query/url or truncates to 120 chars. |
## Server context and paths
| Type | Purpose |
|---|---|
| [`ServerContext`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/ServerContext.swift) | The unifying handle. Holds `id, displayName, kind` (`.local` or `.ssh(SSHConfig)`); exposes `paths`, `makeTransport()`, and high-level helpers (`readText`, `writeText`, `runHermes`, `openInLocalEditor`). The `local` static is the well-known UUID `00000000-0000-0000-0000-000000000001`. |
| `SSHConfig` (nested) | `host, user?, port?, identityFile?, remoteHome?, hermesBinaryHint?` |
| `UserHomeCache` (nested) | Process-wide cache of remote `$HOME` per `ServerID`; probed once via `ssh host echo $HOME`, memoized. |
| [`HermesPathSet`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/HermesPathSet.swift) | All `~/.hermes/*` paths as nonisolated computed properties. Parameterized by `home` and `isRemote`. Includes `hermesBinaryCandidates` (`~/.local/bin/hermes`, `/opt/homebrew/bin/hermes`, `/usr/local/bin/hermes`, `~/.hermes/bin/hermes`) and a `binaryHint` override for remote. |
See [Hermes Paths](Hermes-Paths) for the full path table.
## Config
[`HermesConfig`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/HermesConfig.swift) parses `config.yaml` into a typed tree. About 100+ properties grouped into nested setting structs:
- `display`, `terminal`, `browser`, `voice`, `auxiliary`, `security`, `humanDelay`, `compression`, `checkpoints`, `logging`, `delegation`
- Per-platform: `discord, telegram, slack, matrix, mattermost, whatsapp, homeAssistant`
- Top-level scalars: `model, provider, maxTurns, personality, terminalBackend, memoryEnabled, …`
`HermesConfig.empty` provides safe defaults for empty/missing files.
## Cron, MCP, plugins
| Type | Purpose |
|---|---|
| [`HermesCronJob`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/HermesCronJob.swift) | Job definition with `schedule, prompt, skills?, model?, enabled, state, deliver, nextRunAt?, lastRunAt?, lastError?, preRunScript?, deliveryFailures?, timeoutType?, timeoutSeconds?, silent?`. Nested `CronSchedule` and `CronJobsFile` container. |
| [`HermesMCPServer`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/HermesMCPServer.swift) | MCP server config: `name, transport (.stdio/.http), command?, args, url?, auth?, env, headers, timeouts, enabled, toolsInclude/Exclude, resourcesEnabled, promptsEnabled, hasOAuthToken`. |
| [`MCPServerPreset`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/MCPServerPreset.swift) | Curated presets gallery (Filesystem, GitHub, Postgres, Slack, Linear, Sentry, Puppeteer, Memory, Fetch). |
| `HermesTool`, `HermesToolset`, `KnownPlatforms` | Tool/toolset definitions and a switch for platform icons (cli, telegram, discord, slack, whatsapp, signal, email, homeassistant, webhook, matrix, feishu, mattermost, imessage). |
| `HermesSkill`, `HermesSkillCategory` | Installed skills with `id, name, category, path, files, requiredConfig`. |
## Project dashboards
The dashboard schema lives in [`ProjectDashboard.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/ProjectDashboard.swift):
- `ProjectRegistry``ProjectEntry` (name, path)
- `ProjectDashboard``DashboardSection``DashboardWidget` (`.stat / .progress / .text / .table / .chart / .list / .webview`)
- `WidgetValue` enum with `.string` or `.number` for stat boxes; custom Codable.
- `ChartSeries`, `ChartDataPoint`, `ListItem` for non-scalar widgets.
The full schema is also documented in `scarf/docs/DASHBOARD_SCHEMA.md` in the main repo.
## ACP messages
[`ACPMessages.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/ACPMessages.swift) defines the JSON-RPC envelopes (`ACPRequest`, `ACPRawMessage`, `ACPError`) and the typed events (`ACPEvent.messageChunk / thoughtChunk / toolCallStart / toolCallUpdate / permissionRequest / promptComplete / availableCommands / connectionLost / unknown`). See [ACP Subprocess](ACP-Subprocess) for the protocol details.
## Constants
[`HermesConstants.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/HermesConstants.swift) holds query defaults — `sessionLimit = 100`, `messageSearchLimit = 50`, `toolCallLimit = 50`, `previewContentLength = 100`, `logLineLimit = 200` — plus SQLite C macro replacements and file-size unit constants for display formatting.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+36 -2
@@ -1,6 +1,40 @@
# First Run # First Run
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). The first time you launch Scarf, it opens a single window bound to your local Hermes install at `~/.hermes/`. That window is automatic — there is no setup screen.
## What Scarf expects
For the local server window to work, Hermes must be installed at `~/.hermes/` with at least:
- `~/.hermes/state.db` — SQLite database (created automatically by Hermes on first run).
- `~/.hermes/config.yaml` — runtime config.
- The `hermes` CLI on your `$PATH` (Scarf checks `~/.local/bin/hermes`, `/opt/homebrew/bin/hermes`, `/usr/local/bin/hermes`, and `~/.hermes/bin/hermes` as fallbacks).
If `~/.hermes/` is missing or empty, the Dashboard will tell you so. Install Hermes first per the [Hermes README](https://github.com/hermes-ai/hermes-agent), then relaunch Scarf.
## What you'll see
The window opens to the **Dashboard** — system health, token usage, recent sessions. The sidebar on the left has four groups:
- **Monitor** — Dashboard, Insights, Sessions, Activity
- **Interact** — Chat, Memory, Skills
- **Configure** — Platforms, Personalities, Quick Commands, Credential Pools, Plugins, Webhooks, Profiles, Servers
- **Manage** — Tools, MCP Servers, Gateway, Cron, Health, Logs, Settings
Click any item to open it. Selection lives in `AppCoordinator` — see [Sidebar & Navigation](Sidebar-and-Navigation) for the full list with icons.
## Adding a remote server
To open a window against a remote Hermes install, use **File → Open Server… → Add Server**. You'll need:
- Hostname or alias (resolved via your `~/.ssh/config`)
- Optional user, port, identity file, remote home, and Hermes binary hint
See [Servers & Remote](Servers-and-Remote) for prerequisites on the remote host (`sqlite3`, `pgrep`, SSH key auth, readable `~/.hermes/`).
## Multiple windows
Each Scarf window is bound to exactly one server. Open as many windows as you want — they run with independent state and can be tiled side-by-side.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+92 -3
@@ -1,6 +1,95 @@
# Gateway Cron Health Logs # Gateway / Cron / Health / Logs / Settings
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). The Manage section's operational tools, grouped because they're what you reach for when something needs attention.
## Gateway Control
Start, stop, and restart the messaging gateway. Live status:
- **PID, uptime, connected platforms.**
- Per-platform connection state mirrors the dot you see throughout the app.
- **Pairing management** — view approved users, approve new pairing requests, revoke existing approvals.
The gateway is what brings Hermes onto Telegram / Discord / Slack / etc. Stop it to take the agent offline without quitting Hermes itself.
## Cron Manager
View and edit Hermes scheduled jobs (`~/.hermes/cron/jobs.json`):
| Column | What it shows |
|---|---|
| Name | The job's display name. |
| Schedule | Cron expression or run-at timestamp. |
| State | `enabled` / `paused` / `failed` / `running` with an icon. |
| Last run / next run | Timestamps. |
| Delivery | Channel format like `discord:chat:thread`. |
**Operations** (new in 1.6 — full write support):
- Create a new job — prompt, optional skills, optional model override, schedule, delivery target.
- Edit any field on an existing job.
- Pause / resume.
- Run-now (one-shot trigger outside the schedule).
- Delete.
- Pre-run scripts, delivery-failure tracking, timeout type / seconds, `[SILENT]` indicator for jobs that suppress output.
Edits go through [`ServerContext.writeText`](Architecture-Overview) — atomic, transport-aware.
## Health
Component-level status and diagnostics. Mirrors `hermes status` and `hermes doctor`:
- API key validation per provider.
- Auth provider status.
- External tools availability (browser, terminal, voice).
- "Update available" indicator from [Sparkle](Updating).
**Two buttons:**
- **Run Dump** — captures `hermes dump` output inline.
- **Share Debug Report** — uploads a structured report to Nous support, with a confirmation dialog before sending.
## Log Viewer
Real-time tail for the three main logs at `~/.hermes/logs/`:
- `agent.log` — the agent's loop.
- `errors.log` — errors only.
- `gateway.log` — messaging gateway.
**Filters:**
- **Level** — DEBUG / INFO / WARNING / ERROR / CRITICAL.
- **Component** — Gateway / Agent / Tools / CLI / Cron.
- **Session** — clickable session-ID pills filter the view to one session.
- **Text search.**
Local windows tail with `FileHandle`. Remote windows run `ssh host tail -F` with partial-line buffering so you don't see half-arrived JSON. See [`HermesLogService`](Core-Services).
## Settings
Restructured in 1.6 into a 10-tab layout exposing ~60 previously hidden config fields:
| Tab | What lives here |
|---|---|
| **General** | Updates (Sparkle toggle + manual check), basic preferences. |
| **Display** | Streaming, reasoning visibility, cost display, verbose mode. |
| **Agent** | Model picker (backed by models.dev catalog — 111 providers), max turns, approval mode, reasoning effort. |
| **Terminal** | Terminal backend, Docker / container settings, modal options. |
| **Browser** | Browser backend selection. |
| **Voice** | TTS / STT providers, PTT, silence threshold (default 200ms). |
| **Memory** | `memory_enabled`, `memory_char_limit`, `user_char_limit`, `memory_provider`. |
| **Aux Models** | All 8 auxiliary model tasks (vision, web extract, compression, delegation, etc.). |
| **Security** | Tirith sandbox, command allowlist, website blocklist, redaction. |
| **Advanced** | Logging level / rotation, checkpoints, human-delay simulation, compression thresholds. |
**Backup & Restore** lives at the bottom — wraps `hermes backup` (zips the current profile) and `hermes import` (unzips into the active profile). One-click via `context.runHermes`.
## Related pages
- [Core Services](Core-Services) — the services backing each of these views.
- [Hermes Paths](Hermes-Paths) — where each operational file lives.
- [Updating](Updating) — Sparkle, the appcast, and how the auto-update flow works.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+45 -2
@@ -1,6 +1,49 @@
# Hermes Version Compatibility # Hermes Version Compatibility
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Scarf reads Hermes's SQLite database directly and parses CLI output from `hermes status`, `hermes doctor`, `hermes tools`, `hermes sessions`, `hermes gateway`, and `hermes pairing`. It also speaks ACP (`hermes acp`) for chat. Compatibility comes from automatic schema detection plus tolerant parsing.
## Currently targeted
**Scarf 2.0.x targets Hermes v0.10.0** for the ACP `session/fork` / `session/list` / `session/resume` capabilities used by remote chat. The `CLAUDE.md` in the repo declares this as `Targets Hermes v0.9.0 (v2026.4.13)` for the broader feature set; v0.10.0 is the recommended minimum to get every chat capability working.
## Verified versions
| Hermes | Date | Status |
|---|---|---|
| v0.6.0 | 2026-03-30 | Verified |
| v0.7.0 | 2026-04-03 | Verified |
| v0.8.0 | 2026-04-08 | Verified |
| v0.9.0 | 2026-04-13 | Verified |
| v0.10.0 | 2026-04-18 | Verified (recommended for full 2.0 feature support) |
## How compatibility is maintained
**SQLite schema detection.** [`HermesDataService`](Core-Services) probes for v0.7+ columns (`reasoning_tokens`, `actual_cost_usd`, `cost_status`, `billing_provider`) at open time. When absent, the model fields stay nil and the views render placeholders. Older databases keep working; newer columns light up automatically once the database has them.
**Log line format.** Hermes log lines may carry an optional `[session_id]` tag between the level and the logger name. [`HermesLogService.parseLine`](Core-Services) treats the tag as an optional capture group, so older untagged lines still parse.
**ACP capability gates.** Rich Chat checks for `session/list` and `session/resume` support; if Hermes doesn't advertise them, the resume / picker UI degrades gracefully (you can still chat, just with reduced session navigation).
**CLI output parsing.** Where Scarf parses `hermes` CLI output (Health, Tools, Pairing), the parsers tolerate field reordering and unknown lines.
## What breaks across major Hermes versions
If Hermes ships a major release that changes:
- **Database schema** — columns dropped, table renames — Scarf may need a release. Expected to be infrequent and called out in Hermes release notes.
- **ACP method names or message shapes** — chat would break. Scarf would ship an update with the new methods supported alongside the old.
- **CLI output format** — Health / Tools / Pairing would degrade. Often Scarf can ship a parser update independently.
When in doubt, check the [Health](Gateway-Cron-Health-Logs) view for compatibility warnings (Scarf surfaces the Hermes version it sees and flags mismatches with what it was tested against).
## Reporting a compatibility issue
If a new Hermes release breaks something in Scarf, please file an issue including:
- Output of `hermes --version`.
- Scarf version (Settings → General → About).
- The specific view that's broken and what's wrong.
- The relevant log snippet from `~/.hermes/logs/errors.log` (filter sensitive content first).
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+44 -3
@@ -1,6 +1,47 @@
# Insights and Activity # Insights & Activity
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Three closely related sidebar items live here: **Insights** (analytics roll-up), **Sessions** (conversation browser), and **Activity** (per-tool execution feed). All three read from `~/.hermes/state.db` via [HermesDataService](Core-Services).
## Insights
Aggregated analytics with a time-period selector (7 / 30 / 90 days / all time):
| Section | Content |
|---|---|
| **Overview stats** | Total sessions, messages, tool calls, tokens, active time, average session duration. |
| **Token breakdown** | Input + output + cache-read + cache-write + **reasoning tokens** (v0.7+). |
| **Cost tracking** | Total spend, per-model breakdown, actual vs. estimated when both are available. |
| **Model usage** | Sessions and tokens per model. |
| **Platform breakdown** | CLI vs. Telegram vs. Discord vs. Slack, etc. — sessions and messages per source platform. |
| **Top tools** | Bar chart of the most-called tools with counts and percentages. |
| **Activity heatmap** | Sessions by day-of-week × hour-of-day grid; surfaces "when am I actually using this" patterns. |
| **Notable sessions** | Longest, most messages, most tokens, most tool calls. |
All queries are aggregations over the same `sessions`, `messages`, `tool_calls` tables that drive the other views — no separate analytics pipeline.
## Sessions
The full conversation history browser:
- **List** — every session, ordered by start date DESC. Subagent sessions (those with a `parent_session_id`) are filtered from the main list and accessible by drilling into the parent.
- **Detail panel** — full message stream: user → assistant → tool calls → tool results, with markdown rendering. **Reasoning blocks** (v0.7+) render in a collapsed section.
- **Tool call inspector** — pretty-printed arguments, function name, result. Categorized by `toolKind` (read / edit / execute / fetch / browser / other).
- **Search** — full-text via SQLite's `messages_fts` FTS5 virtual table. Limit defaults to 50 hits.
- **Actions** — rename (`hermes sessions rename`), delete (`hermes sessions delete`), JSONL export (`hermes sessions export`).
Click a session in the Dashboard's "Recent" card to land here with that session pre-selected.
## Activity
The per-tool execution feed — what Hermes did, when, and with what arguments:
- **Filterable by kind and session.** "Show me every browser fetch in session X."
- **Detail inspector** — pretty-printed arguments JSON, tool output (when available), `tool_call_id` for cross-referencing back to the Sessions message stream.
- **Live refresh** — same `HermesFileWatcher`; the feed scrolls as Hermes works.
## Live data freshness
All three views observe the file watcher and re-query when `state.db` changes. Remote windows pull a fresh atomic snapshot via `sqlite3 .backup` (deduped by `SnapshotCoordinator` so Dashboard + Insights + Sessions don't each spawn parallel backups). See [Transport Layer](Transport-Layer) for the snapshot mechanics.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+51 -2
@@ -1,6 +1,55 @@
# Keyboard Shortcuts # Keyboard Shortcuts
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Scarf exposes a focused set of shortcuts. The defining patterns are **window switching** (⌘1…⌘9), **standard dialog flow** (Return / Esc), and a handful of context-specific actions inside views.
## Window / server switching
| Keys | Action | Defined in |
|---|---|---|
| ⌘1 | Open the local server window | [`scarfApp.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/scarfApp.swift) |
| ⌘2 … ⌘9 | Open remote server window 2 through 9 (in registry order) | [`scarfApp.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/scarfApp.swift) |
| ⌘⇧S | Open the Manage Servers picker | [`scarfApp.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/scarfApp.swift) |
| ⌘Q | Quit Scarf | macOS standard, declared in `scarfApp.swift` |
You can have a local window and up to 8 remote windows reachable by ⌘ + number. Beyond that, use Manage Servers (⌘⇧S).
## In-view shortcuts
| Keys | Where | Action |
|---|---|---|
| ⌃B | Chat | Toggle the sessions sidebar inside Chat |
| ⌘S | Personalities (SOUL.md editor) | Save the current SOUL.md |
## Dialog defaults
These follow the macOS convention; Scarf is consistent across every dialog:
| Keys | Behavior |
|---|---|
| Return | Confirm — fires the `.defaultAction` button (Save, Add, Create, Run, etc.). |
| Esc | Cancel — fires the `.cancelAction` button. |
Sheets that follow this pattern include: project create / rename, session rename, MCP server add, Add Server, Add Cron Job.
## Standard system shortcuts
These work in Scarf because they're macOS standards, not because Scarf binds them explicitly:
| Keys | Action |
|---|---|
| ⌘W | Close window |
| ⌘M | Minimize window |
| ⌘N | New window (opens a new local window) |
| ⌘⌥N | New window without tabs (when tabbing is on) |
| ⌘, | Open Settings (Settings is part of `scarfApp.swift`'s scene config) |
| ⌘C / ⌘V | Copy / paste — works inside text fields, message bubbles, log lines |
| ⌘F | Search inside the Sessions browser and Logs view |
## What there isn't
There's no command palette and no global shortcuts beyond ⌘ + number for windows. The sidebar is mouse / arrow-key navigated; there's no ⌘1 / ⌘2 within a window for switching sections.
If you'd like to see additional shortcuts, file an issue — keyboard accessibility is welcome contribution territory.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+66 -3
@@ -1,6 +1,69 @@
# MCP Servers Plugins Webhooks Tools # MCP Servers / Plugins / Webhooks / Tools
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Four sidebar items that all extend what Hermes can do. Grouped here because the workflows are similar.
## MCP Servers
Manage Model Context Protocol servers Hermes connects to. Two ways to add:
- **Curated presets** — Filesystem, GitHub, Postgres, Slack, Linear, Sentry, Notion, Stripe, Puppeteer, Memory, Fetch, and more. Picking a preset fills in the command, args, and the env keys it needs.
- **Custom** — stdio (command + args) or HTTP (URL + optional bearer auth).
**Per-server detail view:**
- Enable / disable toggle.
- Environment variable + header editor — written through [`HermesEnvService`](Core-Services) so existing comments and blanks are preserved.
- Tool include / exclude filters (whitelist / blacklist what the server exposes).
- Resources / prompts toggles.
- Request and connect timeouts.
- OAuth token detection and clearing.
- **Test Connection** runs `hermes mcp test` and surfaces the discovered tool list inline.
A gateway-restart banner appears after config changes that require a reload.
MCP servers are stored in `config.yaml` under the `mcp_servers` key; the model is `HermesMCPServer`.
## Plugins
Hermes plugins are git-cloned into `~/.hermes/plugins/`. Scarf reads the directory directly for reliable state.
**Operations:**
- Install via Git URL or `owner/repo` shorthand.
- Update (pulls latest).
- Remove.
- Enable / disable.
## Webhooks
Create, list, test-fire, and remove webhook subscriptions:
- Endpoint URL, event filter, optional secret.
- **Test fire** sends a synthetic event so you can verify the receiver before going live.
- Detects the "platform not enabled" state and links to the gateway setup.
## Tools
Enable / disable Hermes toolsets per platform.
- Each platform (Telegram, Discord, Slack, etc.) gets its own toolset list.
- Connectivity-aware platform menu: green / orange / grey / red dots match the gateway's reported state.
- Toggling calls `hermes tools enable/disable` via `context.runHermes`.
**Fixed in 1.6:** all 13 platforms now appear here (was previously stuck on CLI only).
## Credential Pools
(Same Configure section, related concept.) Per-provider credential rotation:
- API key + OAuth flow handling. The OAuth flow does URL extraction → browser open → code paste; `--type api-key` is correctly inferred for direct API keys.
- API keys are never stored in UI state — only the last 4 chars are previewed.
- Strategy picker: `fill_first` / `round_robin` / `least_used` / `random`.
## Related pages
- [Gateway / Cron / Health / Logs](Gateway-Cron-Health-Logs) — the gateway is what actually consumes platform / tool config.
- [Hermes Paths](Hermes-Paths) — `~/.hermes/plugins/`, `config.yaml` `mcp_servers` key.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+34 -3
@@ -1,6 +1,37 @@
# Memory and Skills # Memory & Skills
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Two adjacent sidebar items both deal with what Hermes knows. **Memory** is the per-conversation and per-user notes Hermes keeps about you. **Skills** are reusable capabilities you've installed.
## Memory
Live editor for Hermes's two memory files:
- `~/.hermes/memories/MEMORY.md` — project / topic memory.
- `~/.hermes/memories/USER.md` — user memory (preferences, role, recurring context).
**What you get:**
- Side-by-side edit + render with markdown preview.
- **Live refresh** — when Hermes (or you) updates the file from outside, the view reloads via `HermesFileWatcher`.
- **Profile awareness** — if you have multiple Hermes profiles, the picker switches between their memory files.
- **External provider awareness** — when `memory_provider` in `config.yaml` is set to a service like Honcho or Supermemory, the view tells you so and links to the provider's docs.
Edits are written through `ServerContext.writeText` — local: atomic temp + swap; remote: scp + remote `mv`. See [Transport Layer](Transport-Layer).
## Skills
Browse and manage Hermes skills:
- **Installed** — every skill under `~/.hermes/skills/`, grouped by category, with a file content viewer and required-config warnings (skill says it needs `OPENAI_API_KEY` in `.env`? It tells you).
- **Hub** — search the registry catalog (official, skills.sh, well-known, GitHub, ClawHub, LobeHub). Install, check for updates, uninstall.
Operations are wrappers around the `hermes skills` CLI invoked via `context.runHermes(...)`, so they work identically against local and remote servers.
## Related pages
- [Hermes Paths](Hermes-Paths) for the underlying file layout.
- [Personalities](Platforms-Personalities-QuickCommands) for `SOUL.md` editing — closely related to memory but tied to a personality, not a profile.
- [Settings — Memory tab](Gateway-Cron-Health-Logs) for `memory_enabled`, `memory_char_limit`, `memory_provider`.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+49 -3
@@ -1,6 +1,52 @@
# Platforms Personalities QuickCommands # Platforms / Personalities / Quick Commands
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Three separate Configure-section items grouped together because they all shape how Hermes presents itself.
## Platforms
Native GUI setup for all 13 messaging platforms Hermes supports — no more hand-editing `.env` and `config.yaml`:
Telegram, Discord, Slack, WhatsApp, Signal, Email, Matrix, Mattermost, Feishu, iMessage, Home Assistant, Webhook, CLI.
**Per-platform forms:**
- Credentials → `~/.hermes/.env` via [`HermesEnvService`](Core-Services) (preserves comments, supports non-destructive unset by commenting-out).
- Behavior toggles → `~/.hermes/config.yaml` via the typed config struct.
- WhatsApp + Signal pairing use an inline SwiftTerm terminal for QR scan and `signal-cli` daemon management.
Connectivity dots next to each platform reflect the gateway's last reported status:
- **Green** — connected and healthy.
- **Orange** — configured but offline.
- **Grey** — not configured.
- **Red** — error.
## Personalities
A personality is a `SOUL.md` file that shapes Hermes's voice, defaults, and internal rules. Personalities live under `~/.hermes/personalities/<name>/`.
**What you can do here:**
- List defined personalities.
- Pick the active one — written to `personality:` in `config.yaml`.
- Edit `SOUL.md` inline with markdown preview. ⌘S saves.
- Create / rename / delete personalities.
Switching personality takes effect on the next agent turn — no restart needed.
## Quick Commands
Custom `/command_name` shell shortcuts. You define a name, a shell command (with optional arg substitution), and an optional description; the command becomes invocable from anywhere Hermes accepts commands.
**Safety:** the editor scans for dangerous patterns (`rm -rf`, `mkfs`, fork bombs, sudo, suspicious eval) and warns before saving. The check is heuristic — it's a guard against typos, not a sandbox.
Quick Commands live in `config.yaml` under the `quick_commands` key.
## Related pages
- [Memory & Skills](Memory-and-Skills) — memory is profile-scoped, personality is config-scoped.
- [Gateway / Cron / Health / Logs](Gateway-Cron-Health-Logs) — the gateway reads platform configs to decide what to connect to.
- [Hermes Paths](Hermes-Paths) — where `.env`, `config.yaml`, and `personalities/` live.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+44 -3
@@ -1,6 +1,47 @@
# Projects and Profiles # Projects & Profiles
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Two distinct concepts in adjacent sidebar items. **Projects** are agent-generated dashboards for any directory. **Profiles** are isolated Hermes installations.
## Projects
A project is any directory you tell Scarf about — typically a code repo, but anything works. Each project gets a custom dashboard composed of widgets defined in `<project>/.scarf/dashboard.json`.
**Widget types** (from [`ProjectDashboard.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/ProjectDashboard.swift)):
| Type | Purpose |
|---|---|
| `stat` | Single metric: value + label + optional icon and color. |
| `progress` | Progress bar with label. |
| `text` | Markdown / plain text block. |
| `table` | Columns + rows. |
| `chart` | Line / bar / area / pie with `ChartSeries[]` of `ChartDataPoint{x, y}`. |
| `list` | Bulleted list with optional status badges. |
| `webview` | Embedded web view (URL + height). |
**The Hermes pattern:** ask your agent to build and maintain the dashboard for you. "Update `.scarf/dashboard.json` to show test pass rate, lines of code, and the open PR list." Scarf renders the result; the agent maintains it.
The full schema is documented in [`scarf/docs/DASHBOARD_SCHEMA.md`](https://github.com/awizemann/scarf/blob/main/scarf/docs/DASHBOARD_SCHEMA.md) in the main repo.
**Adding a project:** click + in the Projects sidebar, pick a directory. The project is registered in `~/.hermes/scarf/projects.json`; the dashboard JSON lives in `<project>/.scarf/dashboard.json` (which you should add to your project's `.gitignore` if it's user-specific).
## Profiles
A profile is an isolated Hermes installation — separate config, sessions, memory, skills, the lot. Useful for keeping work / personal context separate, or for testing a config change without disturbing your main instance.
**Operations** (all wrap `hermes profile ...` via `context.runHermes`):
- **Switch** — make a profile active. Scarf shows a "restart Scarf to fully apply" reminder because the active profile path is read at launch.
- **Create / rename / delete** — straightforward.
- **Export** — zips the profile directory; useful for backup or moving to a new machine.
- **Import** — unzip into a new profile slot.
Profiles live under `~/.hermes/profiles/`. The currently active profile is whatever Hermes points its `~/.hermes/` symlink (or equivalent) at — Scarf reflects that.
## Related pages
- [Hermes Paths](Hermes-Paths) — `~/.hermes/profiles/` and the projects registry.
- [Memory & Skills](Memory-and-Skills) — memory is profile-scoped.
- [Settings](Gateway-Cron-Health-Logs) — exposes "Backup & Restore" buttons (`hermes backup` / `hermes import`) at the profile level.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+29 -2
@@ -1,6 +1,33 @@
# Release Notes Index # Release Notes Index
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Every Scarf release in chronological order. The notes themselves live in `releases/v<version>/RELEASE_NOTES.md` in the main repo, attached to each GitHub Release as the release body.
| Version | Date | GitHub release | Notes file |
|---|---|---|---|
| **v2.0.1** | 2026-04-20 | [v2.0.1](https://github.com/awizemann/scarf/releases/tag/v2.0.1) | [`releases/v2.0.1/RELEASE_NOTES.md`](https://github.com/awizemann/scarf/blob/main/releases/v2.0.1/RELEASE_NOTES.md) |
| **v2.0.0** | 2026-04 | [v2.0.0](https://github.com/awizemann/scarf/releases/tag/v2.0.0) | [`releases/v2.0.0/RELEASE_NOTES.md`](https://github.com/awizemann/scarf/blob/main/releases/v2.0.0/RELEASE_NOTES.md) |
| **v1.6.2** | 2026-04 | [v1.6.2](https://github.com/awizemann/scarf/releases/tag/v1.6.2) | [`releases/v1.6.2/RELEASE_NOTES.md`](https://github.com/awizemann/scarf/blob/main/releases/v1.6.2/RELEASE_NOTES.md) |
| **v1.6.1** | 2026-04 | [v1.6.1](https://github.com/awizemann/scarf/releases/tag/v1.6.1) | [`releases/v1.6.1/RELEASE_NOTES.md`](https://github.com/awizemann/scarf/blob/main/releases/v1.6.1/RELEASE_NOTES.md) |
| v1.6.0 | 2026-03 | [v1.6.0](https://github.com/awizemann/scarf/releases/tag/v1.6.0) | (no notes file) |
| v1.5.x | 2026-03 | [releases](https://github.com/awizemann/scarf/releases?q=v1.5) | (no notes files) |
## Highlights by major
**2.0** — Multi-server, remote Hermes over SSH, chat UX overhaul (no more flash, no scroll jumping, real error explanations), and a correctness pass that fixed remote WAL spam, stale-snapshot session resume, auto-resume of dead cron sessions, and 230+ Swift 6 concurrency warnings.
**1.6** — Configure section: native GUI for all 13 messaging platforms, Credential Pools with proper OAuth, Model Picker backed by models.dev's 111-provider catalog, 10-tab Settings exposing ~60 previously hidden config fields, plus Personalities, Quick Commands, Plugins, Webhooks, Profiles.
**1.5** — Initial public release series; foundation for everything above.
## After a release
When `scripts/release.sh <version>` completes a full (non-draft) release, this page should be updated:
1. Add a row at the top of the table.
2. Bump the **Latest release** line on [Home](Home).
3. Run `./scripts/wiki.sh commit "docs: index v<version> release"` then `push`.
This is one of the [wiki update triggers](Wiki-Maintenance) that future Claude Code sessions will follow automatically.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+40 -2
@@ -1,6 +1,44 @@
# Roadmap # Roadmap
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). What's next for Scarf. Public, opinionated, subject to change. The internal version of this lives in [`scarf/docs/ROADMAP.md`](https://github.com/awizemann/scarf/blob/main/scarf/docs/ROADMAP.md) — the public wiki version is a distillation.
## Now (2.0.x)
- **Stability.** 2.0 was a big multi-server release. Expect 2.0.x patches focused on edge cases in remote diagnostics, snapshot recovery, ACP reconnection, and correctness.
- **Wiki bootstrap.** This wiki is new — Phase 2 is filling it out, Phase 3 is keeping it in sync (ongoing).
## Near-term
These are the unblocked candidates for 2.1 / 2.2:
- **Test coverage.** A `MockTransport` unlocks unit-testing the service layer; protocol-oriented testing for `HermesEnvService`, `ACPClient` framing, `HermesPathSet`, `HermesConfig` decoding. See [Testing](Testing).
- **Mermaid diagrams in the wiki.** Architecture pages get a lot of value from one good diagram; deferred to keep Phase 1 focused on text.
- **Per-project FSEvents on remote.** Remote currently has one global mtime-poll loop ([HermesFileWatcher](Core-Services) line 84 has a TODO); per-project paths would reduce remote chattiness.
- **More MCP presets.** The curated list grows as MCP ecosystem matures.
## Medium-term
- **Custom commands palette.** A ⌘K-style palette for quick actions across all sidebar sections.
- **Better Insights.** Rolling heatmaps, drill-downs from any chart, exportable summaries.
- **Voice mode polish.** Speaker selection, partial-results display, better handling of long-form dictation.
- **In-app log filtering by structured fields.** Currently text-search; a typed query (level=error AND component=gateway AND session=...) would help.
## Long-term / speculative
- **Versioned docs.** GitHub wikis don't support `/v1.0/` paths natively; could mirror to GitHub Pages with a static-site generator (deferred — see [Wiki Maintenance](Wiki-Maintenance) "Out of scope").
- **DocC → wiki bridge** for auto-generated API reference.
- **Translated wiki pages** if there's demand.
## What we're NOT doing
- **A web version of Scarf.** The whole point is being a native macOS app close to the metal.
- **Background sync / push notifications.** Scarf is a viewer; Hermes runs the agent.
- **Bundled Hermes installer.** Hermes installation belongs in Hermes-land.
- **Closed-source / paid tier.** MIT-licensed, free, will stay that way.
## Suggesting features
Open an issue at <https://github.com/awizemann/scarf/issues> with what you want and why. Star the repo if you'd use it (signal helps prioritization).
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+54 -3
@@ -1,6 +1,57 @@
# Servers and Remote # Servers & Remote
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Scarf 2.0 is multi-server. Each window binds to one Hermes install — your local `~/.hermes/` (synthesized automatically) or any number of remote SSH hosts. Server state lives in `~/Library/Preferences/com.scarf.app.plist` via the `ServerRegistry`.
## Adding a remote server
**File → Open Server… → Add Server.** Fill in:
| Field | Required? | Notes |
|---|---|---|
| Hostname or alias | yes | Resolved via your `~/.ssh/config`. Use whatever you'd type after `ssh`. |
| User | optional | Defaults to your local username if absent. |
| Port | optional | Defaults to 22 (or whatever `~/.ssh/config` provides). |
| Identity file | optional | Specific private key. Otherwise, ssh-agent's loaded keys are tried in order. |
| Remote home | optional | Override `$HOME` if Hermes lives outside the SSH user's home. |
| Hermes binary hint | optional | E.g. `/usr/local/bin/hermes` if not on the SSH user's `PATH`. |
**Test Connection** runs a fast probe before saving. If `state.db` isn't found at `~/.hermes/`, it tries `/var/lib/hermes/.hermes`, `/opt/hermes/.hermes`, `/home/hermes/.hermes`, and `/root/.hermes` (common systemd / Docker layouts) and offers a one-click fill if it finds any.
## Remote prerequisites
The remote host must have:
1. **SSH access** — key-based auth via your local ssh-agent. Scarf never prompts for passphrases; run `ssh-add` once in Terminal before connecting.
2. **`sqlite3`** on the remote `$PATH` — needed for the atomic DB snapshots. Install with `apt install sqlite3` (Ubuntu/Debian), `yum install sqlite` (RHEL/Fedora), or `apk add sqlite` (Alpine).
3. **`pgrep`** on the remote `$PATH` — used by the Dashboard's "is Hermes running" check. Standard on every distro; install `procps` if missing.
4. **`~/.hermes/` readable by the SSH user.** When Hermes runs as a separate user (systemd service, Docker container), the SSH user needs read access to `config.yaml` and `state.db`. Either (a) SSH as the Hermes user, (b) `chmod` Hermes's home to be group-readable and add your SSH user to that group, or (c) set the **Hermes data directory** field when adding the server to point at the right location (e.g. `/var/lib/hermes/.hermes`).
## How remote works under the hood
- Every remote primitive goes through [`SSHTransport`](Transport-Layer), which multiplexes ssh / scp / sftp through one ControlMaster connection.
- `state.db` is read from atomic `sqlite3 .backup` snapshots cached at `~/Library/Caches/scarf/snapshots/<server-id>/state.db`.
- File watching uses 3-second mtime polling.
- Chat uses `ssh -T host -- hermes acp` with JSON-RPC over the tunnel; see [ACP Subprocess](ACP-Subprocess).
## Diagnostics
If the connection pill is green but the Dashboard shows "Stopped", "unknown", or empty values, the SSH user can't read the Hermes state files.
**Manage Servers → 🩺 Run Diagnostics** (or click the yellow "Can't read Hermes state" pill in the toolbar) runs **fourteen** checks in one SSH session: connectivity, `sqlite3` presence, read access to `config.yaml` and `state.db`, the effective non-login `$PATH`, etc. Each failure explains itself with a remediation hint. **Copy Full Report** dumps the whole output for bug reports.
## Switching the active window
- **⌘1** — local server window.
- **⌘2 … ⌘9** — your saved remote servers in order.
- **⌘⇧S** — open the Manage Servers sheet to add / remove / test connections.
See [Keyboard Shortcuts](Keyboard-Shortcuts).
## Related pages
- [Transport Layer](Transport-Layer) for the SSH internals (ControlMaster, snapshot mechanics).
- [ACP Subprocess](ACP-Subprocess) for chat over SSH.
- [Hermes Paths](Hermes-Paths) for what each remote file is.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+84 -2
@@ -1,6 +1,88 @@
# Sidebar and Navigation # Sidebar and Navigation
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Navigation state lives in a single `@Observable` coordinator. The sidebar is a `List` bound to it; feature views observe it. There is no router, no NavigationStack stack tracking, no global state library — just one source of truth for "where am I?".
## AppCoordinator
[`AppCoordinator.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/AppCoordinator.swift) holds three pieces of state:
- `selectedSection: SidebarSection` — defaults to `.dashboard`.
- `selectedSessionId: String?` — optional deep link into the Sessions browser.
- `selectedProjectName: String?` — optional deep link into a project dashboard.
It is injected at the root of each window via `.environment(coordinator)` in `ContextBoundRoot` so any view can read it with `@Environment(AppCoordinator.self) private var coordinator`.
Each Scarf window has its **own** `AppCoordinator` — selection in one window doesn't bleed into another. The coordinator is paired with one [`ServerContext`](Architecture-Overview) for the lifetime of the window.
## SidebarSection
`SidebarSection` is the source of truth for every sidebar item. Each case has a `rawValue` (display name) and an `icon` (SF Symbol name). The 23 cases are grouped into four sidebar headers:
### Monitor (4)
| Section | Icon |
|---|---|
| Dashboard | `gauge.with.dots.needle.33percent` |
| Insights | `chart.bar` |
| Sessions | `bubble.left.and.bubble.right` |
| Activity | `bolt.horizontal` |
### Interact (3)
| Section | Icon |
|---|---|
| Chat | `text.bubble` |
| Memory | `brain` |
| Skills | `lightbulb` |
### Configure (7)
| Section | Icon |
|---|---|
| Platforms | `dot.radiowaves.left.and.right` |
| Personalities | `theatermasks` |
| Quick Commands | `command.square` |
| Credential Pools | `key.horizontal` |
| Plugins | `app.badge.checkmark` |
| Webhooks | `arrow.up.right.square` |
| Profiles | `person.2.crop.square.stack` |
### Manage (8)
| Section | Icon |
|---|---|
| Projects | `square.grid.2x2` |
| Tools | `wrench.and.screwdriver` |
| MCP Servers | `puzzlepiece.extension` |
| Gateway | `antenna.radiowaves.left.and.right` |
| Cron | `clock.arrow.2.circlepath` |
| Health | `stethoscope` |
| Logs | `doc.text` |
| Settings | `gearshape` |
## SidebarView
[`SidebarView.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/SidebarView.swift) is a `List` with hardcoded `Section` headers. Selection is two-way bound to `coordinator.selectedSection`:
```swift
List(selection: $coordinator.selectedSection) {
Section("Monitor") { }
Section("Interact") { }
Section("Configure") { }
Section("Manage") { }
}
.listStyle(.sidebar)
```
There is no search bar, no collapsible-section state to persist — every section is always expanded.
## Routing
`ContentView`'s `detailView` is a single `switch coordinator.selectedSection` over every `SidebarSection` case, returning the right view for the current selection. New features add one case here (see [Adding a Feature Module](Adding-a-Feature-Module)).
## Multi-window
Each window is bound to one `ServerContext` and one `AppCoordinator`. The window menu (and `⌘1…⌘9` keyboard shortcuts) opens additional windows for other servers — see [Keyboard Shortcuts](Keyboard-Shortcuts). Closing a window destroys its coordinator; reopening reads the section back from defaults.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+48 -2
@@ -1,6 +1,52 @@
# Testing # Testing
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). **Honest disclosure:** Scarf's test coverage is minimal. The `scarfTests/` and `scarfUITests/` targets exist but contain placeholder tests only. The project has historically relied on dogfooding — the maintainer runs Scarf against their own daily Hermes install and against a remote dogfooding host.
This page documents what's in place and where contributions would help most.
## Frameworks
When tests are added, the standard is **Swift Testing** (`@Suite` / `@Test` macros), not XCTest. Per the project conventions ([CLAUDE.md](https://github.com/awizemann/scarf/blob/main/CLAUDE.md)):
- Use `@Suite` and `@Test` macros for all new tests.
- Protocol-oriented services for testability — the `ServerTransport` protocol is the obvious mocking seam.
- No timing-dependent tests: use polling with early exit, not `Task.sleep` + assertion.
- Singleton state isolation: call cleanup methods + `await Task.yield()` before assertions.
- No `print()` in production code — use `os.Logger`. `print()` is fine in `#Preview` and test helpers.
## Running
```bash
xcodebuild test -project scarf/scarf.xcodeproj -scheme scarf
```
Or in Xcode: ⌘U.
## What would be high-value to add
If you're looking for a contribution, these are the gaps that would matter most:
1. **`ServerTransport` mock + `LocalTransport` smoke tests** — every service depends on transport, so a `MockTransport` unlocks unit-testing all of them.
2. **`HermesEnvService` round-trip tests** — non-destructive .env editing has tricky comment / blank-line preservation; would benefit from regression coverage.
3. **`ACPClient` JSON-RPC framing tests** — feed canned JSON-RPC byte streams and assert events emitted.
4. **`HermesPathSet` path-resolution tests** — local vs. remote home, binary hint precedence.
5. **`HermesConfig` decoding tests** — load representative `config.yaml` fixtures and check field mapping.
6. **`SSHTransport` shell-quoting tests** — `shellQuote` and `remotePathArg` are correctness-critical and pure functions.
## Manual verification flows
For any change that touches behavior, here's the manual checklist the maintainer runs before tagging a release:
- Open a local window — Dashboard loads, Sessions browser populates, Memory editor opens.
- Open a remote window — same Dashboard / Sessions / Memory, but against the dogfooding host.
- Send a Rich Chat message — receives a streamed response, reasoning shows if the model emits it.
- Edit and save a memory file — change appears in Hermes on next agent turn.
- Run a Cron job — appears in the Cron view, has correct delivery channel.
- Toggle a tool in Tools — `hermes tools enable/disable` runs and the dot color updates.
## Why so little automated coverage?
The app is a thin GUI over Hermes — most behavior depends on (a) the OS file system, (b) SQLite, (c) SSH, (d) a long-running subprocess speaking JSON-RPC. Mocking these well is non-trivial and historically the cost has been higher than the bug rate justified. The transport protocol now makes it cheaper; the gap is finally worth filling.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+120 -2
@@ -1,6 +1,124 @@
# Transport Layer # Transport Layer
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). The `ServerTransport` protocol unifies local and SSH I/O. Services consume `transport.readFile(path)`, `transport.runProcess(...)`, `transport.snapshotSQLite(path)`, etc., without caring whether the bytes come from disk or the wire. Two implementations live under [`scarf/scarf/scarf/Core/Transport/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/Core/Transport): `LocalTransport` and `SSHTransport`.
## Protocol surface
[`ServerTransport.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Transport/ServerTransport.swift) exposes:
**Identity**
- `contextID: ServerID` — UUID; namespaces caches under `~/Library/Caches/scarf/snapshots/<id>/`.
- `isRemote: Bool` — true for `SSHTransport`.
**File I/O**
- `readFile(_ path) -> Data`
- `writeFile(_ path, data:)` — atomic via temp + swap; preserves `0600` mode for `.env`/`auth.json`/`*-tokens.json`.
- `fileExists(_ path) -> Bool`
- `stat(_ path) -> FileStat?` — size, mtime, isDirectory.
- `listDirectory(_ path) -> [String]`
- `createDirectory(_ path)` — idempotent, creates intermediates.
- `removeFile(_ path)` — idempotent.
**Processes**
- `runProcess(executable, args, stdin, timeout) -> ProcessResult` — blocking; captures stdout/stderr; SIGTERM on timeout.
- `makeProcess(executable, args) -> Process` — pre-configured but not yet started; caller owns lifecycle (used by `ACPClient`).
**SQLite snapshots**
- `snapshotSQLite(remotePath) -> URL` — local: returns the path unchanged. Remote: `sqlite3 .backup` on the remote, scp the result down, return a local URL into the snapshot cache.
**Watching**
- `watchPaths(_ paths) -> AsyncStream<WatchEvent>` — yields `.anyChanged` on any change. Local: FSEvents (`DispatchSourceFileSystemObject`). Remote: 3-second mtime polling.
## Errors
[`TransportErrors.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Transport/TransportErrors.swift) defines `TransportError`:
| Case | Cause |
|---|---|
| `hostUnreachable(host, stderr)` | DNS, connection refused, no route. |
| `authenticationFailed(host, stderr)` | SSH key not loaded or rejected. |
| `hostKeyMismatch(host, stderr)` | `~/.ssh/known_hosts` mismatch. |
| `commandFailed(exitCode, stderr)` | Remote command exited non-zero. |
| `fileIO(path, underlying)` | Local FS error. |
| `timeout(seconds, partialStdout)` | Hit `timeout` parameter. |
| `other(message)` | Catch-all. |
Stderr-pattern classification turns raw `ssh` errors into the right case so the UI can render actionable text.
## LocalTransport
[`LocalTransport.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Transport/LocalTransport.swift) — a thin wrapper around `FileManager`, `Process`, and `DispatchSourceFileSystemObject`.
- **Atomic writes:** writes to `<path>.scarf.tmp`, sets `0600` if the filename suggests a secret, then `replaceItemAt` (existing) or `moveItem` (new).
- **Process timeout:** polls every 100ms until deadline; `terminate()` if exceeded.
- **Watching:** opens each path with `O_EVTONLY`, creates a dispatch source for `.write/.extend/.rename`, yields `.anyChanged` on event.
- **Snapshot:** no-op — returns the path unchanged.
## SSHTransport
[`SSHTransport.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Transport/SSHTransport.swift) — every primitive becomes an `ssh`/`scp`/`sftp` invocation, multiplexed over a single ControlMaster connection.
### ControlMaster pooling
Without ControlMaster, every remote call re-authenticates (500ms-2s each). With it, the first call sets up the master socket; subsequent calls reuse the same TCP+crypto session at ~5ms each.
The SSH option set is constructed by `sshArgs(extra:)`:
```
-o ControlMaster=auto
-o ControlPath=~/Library/Caches/scarf/ssh/%C
-o ControlPersist=600 # keep alive 600s after last use
-o ServerAliveInterval=30 # keepalive every 30s
-o ServerAliveCountMax=3 # disconnect after 3 missed
-o ConnectTimeout=10
-o StrictHostKeyChecking=accept-new
-o LogLevel=QUIET # binary-clean stdin/stdout for JSON-RPC
-o BatchMode=yes # ssh-agent only; never prompt
```
`%C` hashes `(host, user, port)` — multiple Scarf windows for the same host share one socket. `closeControlMaster()` issues `ssh -O exit` for clean shutdown.
### Path handling
Two helpers prevent shell-expansion breakage:
- `shellQuote(_:)` — wraps unsafe strings in single quotes, escaping embedded singles as `'\''`. Safe characters (alphanumerics + `@%+=:,./-_`) pass through unquoted.
- `remotePathArg(_:)` — converts `~/...` to `$HOME/...` (because shells don't expand `~` inside quotes) and double-quotes so `$HOME` expands but spaces don't break.
### File I/O over SSH
- `readFile`: `ssh host -- sh -c 'cat <path>'`; classifies "No such file" into typed `fileIO`.
- `writeFile`: scp to `<path>.scarf.tmp`, then remote `mv` — atomic; cleans the orphan on failure.
- `stat`: tries GNU `stat -c "%s %Y %F"`, falls back to BSD `stat -f "%z %m %HT"`.
- `listDirectory`: `ls -A <path>`. `createDirectory`: `mkdir -p`. `removeFile`: `rm -f`.
### Process execution
- `runProcess`: wraps `<exe> <args>` in `sh -c` so paths can use `$HOME`. Inherits `SSH_AUTH_SOCK` from the user's GUI environment so 1Password / Secretive agents work.
- `makeProcess`: returns `/usr/bin/ssh -T <opts> host -- sh -c '<exe> <args>'`. The `-T` disables PTY allocation so stdin/stdout stay binary-clean for JSON-RPC.
### SQLite snapshot
The trickiest operation. The remote runs:
```
sqlite3 "$HOME/.hermes/state.db" ".backup '/tmp/scarf-snapshot-XYZ.db'" && \
sqlite3 '/tmp/scarf-snapshot-XYZ.db' "PRAGMA journal_mode=DELETE;"
```
`.backup` is WAL-safe — it captures a consistent snapshot without blocking writers. The `PRAGMA journal_mode=DELETE` strips WAL mode so the snapshot is self-contained (no `-wal`/`-shm` sidecars). `scp` pulls it to `~/Library/Caches/scarf/snapshots/<id>/state.db`. The remote temp is removed.
### Remote watching
3-second polling: the remote runs a one-liner concatenating mtimes for the watched paths, hashed into a signature. When the signature changes, the stream yields `.anyChanged`. Transient connection drops are tolerated.
### Required tools on the remote
- `sqlite3` for the snapshot operation.
- `pgrep` for the Dashboard's "is Hermes running" check.
- `~/.hermes/` readable by the SSH user.
See [Servers & Remote](Servers-and-Remote) for setup and troubleshooting.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+34 -2
@@ -1,6 +1,38 @@
# Uninstalling # Uninstalling
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Scarf is a self-contained `.app` bundle with no installers, launch agents, or kernel extensions. Removing it is two steps; cleaning up its caches is one more.
## Quit and remove the app
1. Quit Scarf (⌘Q).
2. Drag **Scarf.app** from `/Applications` to the Trash.
That's the minimum. Scarf is gone.
## Clean up Scarf's caches and prefs
If you want a complete uninstall, also remove:
```bash
rm -rf ~/Library/Caches/scarf # remote SQLite snapshots, ssh ControlMaster sockets
rm -f ~/Library/Preferences/com.scarf.app.plist # app preferences + server registry
```
The cache holds atomic SQLite snapshots pulled from remote Hermes hosts and SSH ControlMaster sockets — both are safe to delete; Scarf rebuilds them on demand.
## What Scarf does NOT touch
Scarf reads Hermes's data; it does not own it. The following are **not** removed by uninstalling:
- `~/.hermes/` — your Hermes install, sessions, memory, config, etc.
- `~/.ssh/` — SSH keys and config used to reach remote servers.
- `~/.local/bin/hermes` (or wherever your `hermes` CLI lives).
To uninstall Hermes itself, follow the Hermes documentation — that's a separate process.
## What about the wiki?
Just for completeness: the GitHub wiki at <https://github.com/awizemann/scarf/wiki> is a separate git repo from the main one. Uninstalling the app has no effect on the wiki.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_
+34 -2
@@ -1,6 +1,38 @@
# Updating # Updating
> **TODO: document.** This page is a stub. See [Wiki Maintenance](Wiki-Maintenance). Scarf uses [Sparkle](https://sparkle-project.org/) to deliver automatic updates from a GitHub-Pages-hosted appcast at `https://awizemann.github.io/scarf/appcast.xml`. Each release is EdDSA-signed by the maintainer's private key; Scarf refuses any update whose signature doesn't verify against the embedded `SUPublicEDKey`.
## Automatic updates
By default, Scarf checks for updates on launch and every 24 hours after that. When a new version is available, Sparkle pops up its standard dialog with the release notes and asks whether to install.
To **disable automatic checks**, open **Settings → General → Updates**. You can re-enable later from the same place.
## Manual check
Two ways to force a check:
- **Settings → General → Updates → Check for Updates Now**
- **Menu bar → Check for Updates**
## Beta / draft releases
Scarf does not ship beta channels — the appcast only carries the latest published release. Drafts pushed via `./scripts/release.sh <ver> --draft` are uploaded to GitHub but not added to the appcast, so they don't reach existing installs until promoted manually.
## Downgrading
There's no in-app downgrade. To run an older build:
1. Quit Scarf.
2. Move the current `Scarf.app` to the Trash.
3. Download the older zip from [Releases](https://github.com/awizemann/scarf/releases) and drag it to `/Applications`.
4. Disable **automatic checks** in **Settings → General → Updates**, otherwise Sparkle will offer to update you back.
App preferences live in `~/Library/Preferences/com.scarf.app.plist` and are forward/backward compatible across versions in normal cases.
## Update fails to apply
If Sparkle reports a signature mismatch or download error, see the [Health](Gateway-Cron-Health-Logs) view for details. Re-trigger the check; if it persists, file an issue with the Sparkle log content.
--- ---
_Last updated: 2026-04-20 — stub_ _Last updated: 2026-04-20 — Scarf v2.0.1_