Compare commits

..

126 Commits

Author SHA1 Message Date
Alan Wizemann e8fcd699f2 Merge pull request #38 from awizemann/claude/beautiful-goldstine-6aa6c5
refactor(settings): remove unused providers list
2026-04-23 01:15:17 +01:00
Alan Wizemann d82b28258d refactor(settings): remove unused providers list
The hardcoded `providers` array in SettingsViewModel was never referenced — no view reads `viewModel.providers`; the Model picker uses the models.dev catalog via `ModelCatalogService.loadProviders()` and Provider is shown as a `ReadOnlyRow` in the General tab. Leaving the dead list around makes issues like #33 look plausible (users reasonably guess a stale enum is normalising `openai-codex` → `openai` on save, which the code does not actually do).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 02:13:49 +02:00
Alan Wizemann 323a1e55f4 Merge pull request #34 from awizemann/fix/code-review-apr22
fix: address code-review findings from Apr 22 commits
2026-04-23 00:03:18 +01:00
Alan Wizemann 959a68b707 fix: address code-review findings from Apr 22 commits
Three follow-ups from reviewing 1989fee (sidebar-width persist) and
4163595 (default server on launch):

- `SplitViewAutosaveFinder` hardcoded `"ScarfMainSidebar"` for every
  window. Since Scarf's `WindowGroup` spawns one window per `ServerID`,
  all windows shared the same `NSSplitView.autosaveName` — AppKit
  documents that name as required-unique, and in practice per-window
  widths collapsed onto a single UserDefaults key. Thread the window's
  `ServerContext` in through `@Environment(\.serverContext)` (already
  wired at `WindowGroup` construction) and suffix the name with the
  server UUID.
- `setDefaultServer` fired `onEntriesChanged`, whose sole consumer is
  `ServerLiveStatusRegistry.rebuild()` for menu-bar fanout. Flipping a
  default flag doesn't change the set of servers; the callback was
  semantic noise. Drop the call — SwiftUI views still redraw on the
  flag flip via `@Observable`'s tracking of `entries`.
- The filled-yellow star in `ManageServersView` had a no-op action
  inside `if !isDefault { ... }` but still animated its pressed state
  on click. Replace the conditional with `.disabled(isDefault)` so the
  row is visually inert when it already is the default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:59:58 +02:00
Alan Wizemann e76fbf9937 chore: audit follow-ups from plan review
Four small fixes surfaced by a side-by-side plan-vs-shipped pass:

- README.md: adds the Template Catalog section the plan called out —
  links to the live site URL, the install flows (web / file / Finder),
  and templates/CONTRIBUTING.md for authors. Placed right before the
  existing Contributing section, with a catalog-specific cross-link at
  the end of that section too.
- CLAUDE.md: adds the Template Catalog section so future agent sessions
  know the regenerator pipeline exists, how it relates to release.sh +
  wiki.sh, and what the schema-sync rule is when DashboardWidget or
  ProjectTemplateManifest change.
- scarf/scarfTests/ProjectTemplateTests.swift: fixes the stale
  ProjectTemplateExampleTemplateTests docstring still referencing
  `examples/templates/` (the example moved to `templates/awizemann/`
  in 70f7cea).
- .github/workflows/validate-template-pr.yml: untangles the self-
  contradictory Python-version comment. The validator is 3.9+
  compatible; CI uses 3.11 for faster runner caching. Same stdlib
  surface, same code paths — just clearer about why.

All tests still green: 22 Swift tests in 7 suites, 16 Python tests,
catalog check passes on the site-status-checker example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:35:46 +02:00
Alan Wizemann c9b8da9ec5 feat(ci): validate template submissions on PR + tailored checklist
Adds the CI gate that runs on every PR touching templates/, the catalog
validator, or its tests. The Action:
- runs tools/test_build_catalog.py (catches drift between validator +
  its own test suite on the same PR that introduces the drift)
- runs tools/build-catalog.py --check (validates every shipped .scarftemplate
  against the same invariants ProjectTemplateService.verifyClaims enforces
  at install time)
- posts a PR comment with the last 3 KB of the validator log on failure,
  so contributors see the specific mismatch without hunting through the
  Actions UI

.github/PULL_REQUEST_TEMPLATE/template-submission.md is the author-facing
checklist that mirrors templates/CONTRIBUTING.md. Opt-in via the
?template=template-submission.md compare URL (documented in the
contribution guide). CONTRIBUTING.md now links both the PR template and
the workflow file so authors know what to expect.

Phase 4 closes the community loop — from this commit on, a stranger can
fork the repo, follow templates/CONTRIBUTING.md, push a PR, and get
deterministic green/red feedback before a maintainer ever looks at it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:35:46 +02:00
Alan Wizemann 6175bee27d feat(site): dogfood the Scarf dashboard format as the catalog website
Adds site/ with vanilla HTML + CSS + ~300 lines of JavaScript that
renders ProjectDashboard JSON directly in the browser. Each template's
detail page shows a live preview of the exact dashboard the user will
get post-install — the catalog IS the dogfood.

site/widgets.js mirrors the Swift widget dispatcher:
- stat (big number + colored icon + optional subtitle)
- progress (0..1 bar)
- text with inline markdown subset (headings, bold/italic, inline code,
  code fences, bullet + numbered lists, links)
- table (plain HTML)
- list (with up/down/unknown status badges)
- chart (SVG line + bar — no Chart.js dependency)
- webview (sandboxed iframe)
- unknown (placeholder so the page doesn't silently omit widgets)

Plus the renderMarkdown helper used by the template detail page to
display the bundle's README.

site/index.html.tmpl + site/template.html.tmpl are substitution-only —
the Python regenerator swaps {{CARDS}}, {{COUNT}}, {{COUNT_PLURAL}},
{{NAME}}, {{DESC}}, {{VERSION}}, {{AUTHOR_HTML}}, {{TAGS_HTML}},
{{INSTALL_URL_ENCODED}}, {{SCARF_INSTALL_URL}}. The detail page fetches
dashboard.json + README.md at page load and hands them to widgets.js.
No client-side framework, no bundler, no npm.

site/styles.css: minimal CSS with scarf green accent, prefers-color-
scheme dark support, responsive at 680px. One file, ~280 lines.

build-catalog.py extended to copy dashboard.json + README.md out of each
bundle into its detail dir so widgets.js can fetch them without
reaching across directories (and so gh-pages doesn't need to serve zip
contents at request time).

Two new Python tests: end-to-end site rendering (both cards, install
URL wiring, static asset copy, per-template dashboard + README copy)
and the {{COUNT_PLURAL}} singular-vs-plural flip. 16/16 Python tests
green.

Smoke-tested locally with python3 -m http.server: every endpoint
(index, catalog.json, detail HTML, per-template dashboard.json + README,
widgets.js) returns 200. The .gh-pages-worktree/appcast.xml +
.gh-pages-worktree/index.html are untouched — the catalog is purely
additive under /templates/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:35:46 +02:00
Alan Wizemann 11732baa3c feat(catalog): stdlib-only Python validator + regenerator for templates/
Adds the catalog pipeline without introducing any external dependencies.
tools/build-catalog.py walks templates/<author>/<name>/, validates every
shipped .scarftemplate against its manifest (same invariants Swift's
ProjectTemplateService.verifyClaims enforces at install time), and emits
templates/catalog.json for the frontend to read.

Validator invariants:
- Required bundle files: template.json, README.md, AGENTS.md, dashboard.json
- contents claim cross-checked against actual zip entries (instructions,
  skills, cron count, memory appendix)
- dashboard.json widget types restricted to the vocabulary the Swift
  renderer knows
- Manifest id author component must match the template directory
- 5 MB bundle-size cap on submissions (installer's own cap is 50 MB)
- High-confidence secret patterns (private keys, GitHub PATs, Slack tokens,
  AWS access keys, OpenAI/Anthropic keys) block the bundle
- staging/ source tree must match the built bundle byte-for-byte — catches
  the common failure mode of editing staging/ but forgetting to rebuild

scripts/catalog.sh wraps the Python script with check/build/preview/serve/
publish subcommands, mirroring the scripts/wiki.sh shape. publish adds a
second-pass hard-pattern secret scan on the rendered gh-pages output so
template prose can't leak credentials even if the Python scan missed them.

tools/test_build_catalog.py has 14 unit tests covering the main validator
paths (minimal-valid, missing-AGENTS, content-claim mismatch, author
mismatch, oversized bundle, unknown widget type, secret detection,
staging-drift detection, missing bundle, catalog.json shape, and a real-
bundle end-to-end check against templates/awizemann/site-status-checker).
Python 3.9 compatible (Xcode's bundled python3), so no runtime needs
installing.

templates/catalog.json committed as the first generated aggregate index;
maintainers regenerate on merge by running `./scripts/catalog.sh build`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:35:46 +02:00
Alan Wizemann d8a0a89db2 feat(templates): promote examples/ to templates/<author>/<name>/ catalog layout
Set up the catalog directory structure this branch will fill with
community templates. The existing site-status-checker example moves
from examples/templates/ to templates/awizemann/site-status-checker/
(tracked by git as a rename so history is preserved). The examples/
directory is removed.

New top-level docs:
- templates/README.md — landing for folks browsing the catalog on
  github.com. Lists the current templates and points at the live site.
- templates/CONTRIBUTING.md — author-facing submission walkthrough.
  Requires AGENTS.md, pre-flight with tools/build-catalog.py --check
  (added in the next commit), one template per PR, don't edit
  catalog.json (maintainer regenerates it post-merge).

ProjectTemplateExampleTemplateTests.locateExample updated to search
templates/<author>/<name>/ instead of examples/templates/ — the test
still walks up from #filePath to find the repo root so it works in
both xcodebuild and Xcode IDE test runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:35:46 +02:00
Alan Wizemann 38c075d61d docs: ship site-status-checker example template + v2.2.0 release notes
First installable template demonstrating the format:
- Dashboard with stat widgets (up/down/last-checked) + configured-sites
  list + quick-start markdown.
- Cross-agent AGENTS.md with the full cron-prompt contract so any agent
  that reads agents.md (Claude Code, Cursor, Codex, Aider, Jules,
  Copilot, Zed, …) picks up the behavior on first run.
- Cron job (0 9 * * *) that ships paused with the [tmpl:…] tag, pinging
  a user-editable sites.txt and writing results to status-log.md.
- First-run bootstrap logic in AGENTS.md: if sites.txt doesn't exist
  yet the agent creates it with two placeholder URLs, then proceeds.

Plus examples/templates/README.md explaining the staging/ layout,
authoring conventions, and how to rebuild a bundle after editing. CI
validates the bundle via ProjectTemplateExampleTemplateTests so drift
between staging/ and the built .scarftemplate fails on every build.

v2.2.0 release notes cover the full feature surface including the
install preview sheet, scarf:// + file:// URL handling, skills
namespacing, cron-job tagging, memory-block markers, and the
lock-driven uninstall flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:35:46 +02:00
Alan Wizemann c800b93804 feat: project templates v1 (install + uninstall + export + URL handler)
Shareable `.scarftemplate` bundle format lets users package a project's
dashboard, cross-agent AGENTS.md, optional per-agent instruction shims,
optional namespaced skills, optional tagged cron jobs, and an optional
memory appendix into a single zip that anyone can install with one click.

Core:
- Bundle format + manifest schema v1 (template.json with contents claim
  cross-checked against zip entries to prevent hidden files).
- ProjectTemplateService inspects + validates + builds an install plan.
- ProjectTemplateInstaller executes plans with transport-routed I/O so
  the v1 local-only flow extends cleanly to remote ServerContexts later.
- ProjectTemplateExporter builds bundles from existing projects with
  user-selected skills + cron jobs.
- ProjectTemplateUninstaller reverses installs using template.lock.json.
  Only lock-tracked files are removed; user-added files are preserved.

UI:
- Templates menu in Projects toolbar: Install from File, Install from
  URL, Export as Template.
- Preview-and-confirm sheets for install, uninstall, and export with
  full diff of what will be written/removed before anything runs.
- Right-click context menu on project list + dashboard header button
  for uninstall (only shown when template.lock.json exists).

Deep link + file associations:
- scarf:// URL scheme registered; onOpenURL in scarfApp.swift routes
  scarf://install?url=https://... and file:// URLs for .scarftemplate
  files to the install sheet.
- Custom UTType com.scarf.template registered so Finder shows the file
  with a Scarf icon and double-click opens the install preview.
- Cold-launch race fix: .task picks up any URL staged on the router
  before the onChange observer was installed.

Safety:
- Never writes to config.yaml, auth.json, sessions, or credentials.
- Cron jobs ship paused with a [tmpl:<id>] name prefix.
- Skills install to a namespaced ~/.hermes/skills/templates/<slug>/ dir
  so they never collide with user-authored skills.
- Memory appendix is wrapped in scarf-template:<id>:begin/end markers
  for clean removal during uninstall.
- Download cap: 50 MB for URL-fetched templates, enforced on the actual
  on-disk file size after download so chunked transfers can't bypass it.

Tests: 22 tests in 7 suites cover manifest parsing, claim verification,
URL routing (scarf:// + file://), end-to-end install and uninstall
against a minimal bundle (projects registry is snapshotted + restored),
user-added file preservation, and exporter round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:35:46 +02:00
Alan Wizemann 7311320bfd Merge pull request #30 from awizemann/claude/issue-26-default-server
Let users pick the default server opened on launch (#26)
2026-04-22 22:52:33 +01:00
Alan Wizemann 4663697942 Merge pull request #29 from awizemann/claude/issue-26-sidebar-width
Persist sidebar width across launches (#26)
2026-04-22 22:44:57 +01:00
Claude 41635955b0 feat: let users pick the default server opened on launch (#26)
Repurposes the previously-unused ServerEntry.openOnLaunch flag so users
can nominate Local or any registered remote as the server Scarf opens
into when a fresh window has no prior binding (first launch or File →
New Window).

- ServerRegistry gains `defaultServerID` (returns the flagged entry's
  ID or falls back to Local) and `setDefaultServer(_:)` (flips the flag
  on the named entry and clears it elsewhere, then persists).
- ScarfApp's WindowGroup defaultValue closure now returns
  `registry.defaultServerID` instead of hardcoded `ServerContext.local.id`.
- ManageServersView gains a Local row at the top of the list plus a
  star button per row: filled yellow on the current default, outline on
  the others. Click to promote.

Backward compatible: the openOnLaunch field was already in the persisted
schema (default false), so existing servers.json files load unchanged —
Local remains the default until the user picks otherwise.

Refs #26
2026-04-22 11:00:32 +00:00
Claude 1989feee22 feat: persist sidebar width across launches (#26)
Wire an NSSplitView autosave name into NavigationSplitView's underlying
AppKit split view so the sidebar's drag-to-resize position is remembered
in UserDefaults and restored on next launch.

SplitViewAutosave.swift installs an invisible NSViewRepresentable that
walks up the view hierarchy from the sidebar, finds the enclosing
NSSplitView, and assigns autosaveName = "ScarfMainSidebar". AppKit
handles persistence from there — no manual UserDefaults or @AppStorage
plumbing needed.

ContentView also gets navigationSplitViewColumnWidth(min:ideal:max:)
bounds so first-launch (before any autosave exists) lands at a sensible
240pt ideal within a 180–360pt range.

Refs #26
2026-04-22 10:58:34 +00:00
Alan Wizemann 8773254d11 chore: accept safe parts of Xcode recommended-settings migration
Xcode 26.x suggested an upgrade pass that included a critical regression:
ENABLE_APP_SANDBOX = YES on the main app, which would silently break every
view that reads ~/.hermes/ (state.db, config.yaml, memory files, skills,
logs). Scarf is architected sandbox-off per CLAUDE.md — reverted.

Kept the benign pieces:

- DEAD_CODE_STRIPPING = YES on all targets (stock modern optimization)
- CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES at project level —
  static analyzer warning for un-localizable call sites; directly
  relevant to the i18n work in 2.1.0 and will flag regressions of the
  exact patterns just cleaned up
- STRING_CATALOG_GENERATE_SYMBOLS = YES hoisted to project level
  (was already set at target level; hoisting is a no-op functional
  change but Xcode prefers it inherited)
- Scheme file LastUpgradeVersion bumped to 2620 to match current Xcode

Rejected:
- ENABLE_APP_SANDBOX = YES (critical — would break app file access)
- ENABLE_RESOURCE_ACCESS_AUDIO_INPUT / RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION
  build settings (Xcode's new form replacing the entitlements file;
  keeping the entitlements file as the single source of truth since
  every release 1.x → 2.1.0 shipped and notarized with that form)
- LastUpgradeCheck = 2620 (Xcode dropped 2630 → 2620; cosmetic revert)

v2.1.0 was released before this Xcode pass so no rebuild needed — the
downloaded zips and Sparkle appcast entry are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:27:55 -07:00
Alan Wizemann a1aa653a33 chore: Bump version to 2.1.0 2026-04-20 18:46:47 -07:00
Alan Wizemann e256196397 chore: commit shared Xcode scheme
The scarf scheme existed in every local Xcode session (Xcode auto-creates
it from xcschememanagement's ^#shared#^ entry on first open), but was
never actually committed to the repo. Release v2.1.0 hit the resulting
"project contains no schemes" error on headless xcodebuild archive after
the build/ cache was cleaned. Committing the scheme itself so future
headless builds work from a fresh clone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:46:36 -07:00
Alan Wizemann 50880efe81 docs: prep v2.1.0 release notes + README language badge
Pre-release prep so that when `./scripts/release.sh 2.1.0` runs on main,
the notes file is already in tree (script's `git add` is then a no-op,
bump commit contains only the pbxproj version change).

- README gains a 2.1 "What's New" section covering translations + the
  chat slash-menu; 2.0 moves down to "Previously".
- Badge row gains a language list line.
- Full release notes at releases/v2.1.0/RELEASE_NOTES.md — covers the
  three stacked i18n PRs (infra, audit burn-down, translations) and the
  chat slash-menu work merged in parallel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:39:51 -07:00
Alan Wizemann b1bc7e8494 Merge pull request #25 from awizemann/translations-initial
feat(i18n): initial translations for 6 languages + contributor workflow
2026-04-20 18:37:06 -07:00
Alan Wizemann f47034d4ad fix(i18n): localize sidebar, settings tabs, and settings section titles
Three connected bugs where the Label/SettingsSection APIs took a `String`,
which routes through the StringProtocol overloads and bypasses localization
entirely. Identified by the user after testing zh-Hans / de / fr — the
sidebar menu items, Settings tab bar, and Settings section headers all
remained English under any App Language override.

- SidebarSection now exposes displayName: LocalizedStringResource; SidebarView
  builds Label via the Text/Image builders so the catalog key is actually
  used.
- SettingsTab gets the same displayName treatment; the .tabItem Label builds
  through the Text/Image builder too.
- SettingsSection.title changes from String → LocalizedStringKey so literal
  call sites (all ~20 of them) now extract into the catalog. Two call sites
  that were passing String variables (PlatformsView, CredentialPoolsView) are
  wrapped via LocalizedStringKey(...) — brand/provider names fall through to
  English as before. AuxiliaryTab's static task list gets a LocalizedStringKey
  column so its section titles extract too.

This change newly extracts 65 previously-invisible section-title keys into
the catalog; translations added for all six locales. Catalog: 575 → 644
source keys, each locale translated for 583 of them (brand names / protocol
names / format-only keys intentionally fall through).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 03:32:32 +02:00
Alan Wizemann 1726a613a5 feat(i18n): add translations for zh-Hans, de, fr, es, ja, pt-BR
Ships first-pass AI translations for six locales on top of the existing
English base, plus a simple JSON-per-locale contributor workflow so new
languages can land as a single PR.

- 518 keys translated per locale (proper nouns / brand names / format-
  only strings left to fall back to English by design — see the
  "Non-blocking (intentional verbatim)" section of scarf/docs/I18N.md).
- Per-locale source-of-truth lives in tools/translations/<locale>.json;
  tools/merge-translations.py writes them into Localizable.xcstrings
  and is idempotent (re-runnable as translators iterate).
- InfoPlist.xcstrings (macOS microphone permission prompt) translated
  for all six locales.
- knownRegions expanded: zh-Hans, de, fr now join by es, ja, pt-BR.
- CONTRIBUTING.md gains an "Adding a Language" section documenting the
  fork → JSON → merge → PR flow. Native-speaker reviews welcome.

Closes #13 (the original ask: Simplified Chinese support).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:16:41 -07:00
Alan Wizemann de34a80807 Merge pull request #24 from awizemann/multi-language
feat(i18n): close silently un-localizable sites (Phase 1b)
2026-04-20 17:52:18 -07:00
Alan Wizemann d9a25b3997 Merge pull request #22 from awizemann/claude/pedantic-kare-1edf13
feat(i18n): enable String Catalog + locale-aware numeric formatters
2026-04-20 17:51:09 -07:00
Alan Wizemann b40182f2da feat(i18n): close silently un-localizable sites from the audit
Burns down the follow-ups tracked in scarf/docs/I18N.md so that future
translation passes (Phase 2+) don't see English leak through ternary UI
copy, enum rawValue displays, or fixed-format strings.

- Ternary status copy: Text(cond ? "A" : "B") → cond ? Text("A") : Text("B")
  (each branch routes through LocalizedStringKey). Covers Health, Chat
  (voice/TTS/recording/ACP status), Profiles, MCPServer test result,
  SignalSetup, QuickCommands header.
- Enum .rawValue displays: LogFile, LogComponent, DashboardTab, Skills.Tab,
  InsightsPeriod, ToolKind, AuthType each expose a
  displayName: LocalizedStringResource. LogEntry.LogLevel stays verbatim
  (technical jargon — DEBUG/INFO/ERROR/… are industry-standard).
- displayName passthroughs: HermesToolPlatform, ServerRegistry.Entry,
  MCPServerPreset wrapped with Text(verbatim:) at call sites (brand names
  and user data, not UI chrome). MCPTransport.displayName promoted to
  LocalizedStringResource.
- Composite format strings: ModelPickerSheet "ctx" suffix, InsightsView
  "tokens" suffix and MCPServerTestResultView "%.1fs · %d tools" rewritten
  as Text("\(arg) suffix") LocalizedStringKey. Percent display uses
  .formatted(.percent) after /100.
- Day-of-week chart now sources from Calendar.current.shortWeekdaySymbols,
  re-indexed for the existing Mon=0 data model.
- ConnectionStatusPill's label + tooltip return Text (not String) so the
  .help(Text) / direct-render paths localize correctly.
- Catalog re-synced: 545 → 575 keys (+30 from new ternary branches and
  enum displayName values).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:40:56 -07:00
Alan Wizemann 6817c95681 chore(i18n): sync catalog after rebasing onto chat slash-menu work
Picks up 7 new Text("…") keys introduced by a68e0c5 and c8208de
(loading state copy, slash-menu empty states, argument-hint placeholder).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:25:16 -07:00
Alan Wizemann 89748fdfee feat(i18n): enable String Catalog + locale-aware numeric formatters
Lays the groundwork for zh-Hans / de / fr translations on an English base.
No user-visible English-locale behavior changes. See scarf/docs/I18N.md for
the full plan and remaining audit follow-ups.

- Localizable.xcstrings seeded with 538 keys auto-extracted via
  `xcstringstool sync` from the Swift sources
- InfoPlist.xcstrings carrying NSMicrophoneUsageDescription
- knownRegions += zh-Hans, de, fr
- Currency / byte-count / compact-number String(format:) sites migrated to
  Locale.current-aware .formatted() style (currency, byteCount(.file),
  compactName notation) — previously rendered POSIX separators + English
  unit names regardless of user locale

Refs #13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:24:29 -07:00
Alan Wizemann c8208dedb1 fix(chat): slash-menu filter, auto-scroll on send/complete, loading state
- Slash menu: filter at the parent and pass the pre-filtered list to
  SlashCommandMenu (pure-prefix match, no description fallback). Adds
  `.id(menuQuery)` to force a fresh view on every query so SwiftUI can't
  render stale props — this was the cause of "typing /mo still shows
  /help" (the old description fallback plus a cached child view kept
  /help pinned regardless of query).
- Auto-scroll to bottom when the user submits a message and again when
  the prompt completes. `.defaultScrollAnchor(.bottom)` handles slow
  streaming fine, but rapid slash-command responses outran the anchor
  and left the response off-screen.
- Loading state: add `ChatViewModel.isPreparingSession` (true during
  Starting / Creating / Loading / Reconnecting). While true, the message
  list swaps its placeholder for a ProgressView — non-blocking, just a
  view inside the ScrollView.
- Center the empty-state placeholder properly: replace
  `.padding(.vertical, 80)` with Spacers inside
  `.containerRelativeFrame(.vertical)` so the placeholder sits in the
  true vertical center of the chat pane at any window size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:15:46 -07:00
Alan Wizemann a68e0c5f42 feat(chat): slash-command menu + scroll/layout fixes
- Add floating slash-command menu driven by ACP available_commands_update
  and user-defined quick_commands from config.yaml. ↑/↓ navigate, Tab or
  Enter completes, Esc dismisses. Commands with argument hints insert a
  trailing space so the user can type the argument.
- New HermesSlashCommand model carries name/description/argumentHint/source;
  RichChatViewModel stores ACP + quick_commands separately and merges them
  for the menu. QuickCommandsViewModel exposes a reusable static loader.
- Menu renders as a sibling above the input HStack (not a popover or
  overlay) — guaranteed to render regardless of focus/z-order quirks.
- Hide the dedicated /compress button once the menu has more than one
  command; keep it as a fallback when only /compress is advertised.
- Fix long-standing "session loads with whitespace, must scroll up to see
  chat" bug by switching LazyVStack → VStack in RichChatMessageList.
  LazyVStack's estimated row heights were fooling .defaultScrollAnchor(.bottom)
  into overshooting real content; VStack measures every row upfront so the
  anchor has real heights to work with.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:35:53 -07:00
Alan Wizemann 0384c6ef17 chore: Bump version to 2.0.2 2026-04-20 15:46:07 -07:00
Alan Wizemann f36fb55ebe test(ssh): regression tests for ControlPath socket-limit invariants
Two tests pinning the invariants that were violated / introduced
by the #19 / PR #20 fix:

- controlDirPathFitsMacOSSocketLimit: asserts dir + '/' + 64-char
  %C hash + NUL <= 104 bytes. Would have caught the original
  Caches-based path landing at 105 bytes for users with longer
  $HOME strings.

- controlDirPathIsPerUser: asserts the path includes the current
  uid, pinning the per-user-isolation invariant against any future
  refactor that drops it (since /tmp is shared across all local
  users).

scarfTests was a stub before this — these are the suite's first
real tests.
2026-04-20 15:45:29 -07:00
Alan Wizemann 1823160546 fix(ssh): defensive ControlPath dir + sweep stale sockets
Layered hardening on top of the /tmp ControlPath move from #20:

- ensureControlDir uses POSIX mkdir(0700) + lstat instead of
  createDirectory + setAttributes. Closes the /tmp pre-creation
  TOCTOU: any local user can pre-create /tmp/scarf-ssh-<uid>, and
  the old code would silently fail to chmod a hostile dir back to
  0700 (since we wouldn't own it). Now we refuse to use a dir that
  isn't a real directory we own with mode 0700, and log via
  os.Logger.

- sweepStaleControlSockets removes ControlMaster socket files
  older than 30 minutes from controlDirPath() at app launch.
  Symmetric to sweepOrphanSnapshots — keeps /tmp/scarf-ssh-<uid>/
  from accumulating crashed-master / unclean-exit orphans
  indefinitely until reboot. The 30-min threshold (vs ControlPersist's
  10 min) ensures any concurrent Scarf instance's live sockets
  are untouched.
2026-04-20 15:45:20 -07:00
Alan Wizemann d2a447fcc4 docs: add GitHub wiki + scripts/wiki.sh helper with secret-scan
Public docs now live at https://github.com/awizemann/scarf/wiki (separate
git repo cloned to .wiki-worktree/, mirroring the .gh-pages-worktree/
pattern). Internal dev notes stay in scarf/docs/.

scripts/wiki.sh wraps pull/commit/push with a two-pass secret-scan: hard
patterns (token regexes + private-key headers + a user-maintained
scripts/wiki-blocklist.txt) abort with non-zero exit; soft assignment
patterns (api_key=…, password=…, token=…) warn and require --force-terms.

CLAUDE.md gains a Wiki section listing the update triggers (new feature,
new service, architecture change, Hermes version bump, full release,
keyboard/sidebar change) and the workflow. CONTRIBUTING.md points
external contributors at the wiki Edit button or a direct clone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:32:47 -07:00
Alan Wizemann 76bfeb34d4 chore: Bump version to 2.0.1 2026-04-20 15:32:47 -07:00
Alan Wizemann 85a4ec0e14 Merge pull request #20 from aliatx2017/fix/controlpath-too-long
fix: ControlPath too long for Unix socket on macOS
2026-04-20 15:08:40 -07:00
Alan Wizemann 1453c7a841 Merge fix/issue-19-ssh-diagnostics into main — v2.0.1 hotfix
Closes #19 (remote SSH connections showed connected but every view
read as empty). Eight commits bring:
- Result-returning readers in HermesFileService that surface errors
  instead of silently returning nil
- HermesDataService.open records lastOpenError with humanized hints
- Dashboard orange banner when remote reads fail
- New Remote Diagnostics sheet (14-probe checklist, stethoscope icon)
- Yellow 'degraded' pill state for 'connected but can't read' case
- Auto-suggest remoteHome in Test Connection for systemd/Docker
  installs at /var/lib/hermes/.hermes etc.
- Log-noise suppression for expected 'No such file' reads
- Diagnostics script pipes via stdin to sh -s (not sh -c argv), so
  multi-line scripts run in one sh process with variable scope
- Pill UX: state-specific SF Symbol instead of dot, no custom
  background, centered via .principal
- README 'Remote setup requirements' + troubleshooting section

Investigation notes + deferred follow-ups recorded in the session
transcript. See releases/v2.0.1/RELEASE_NOTES.md for the full
user-facing breakdown.
2026-04-20 14:27:11 -07:00
Alan Wizemann bd21a539e6 docs: update v2.0.1 release notes for diagnostics fixes + pill UX
Reflect the three post-initial-commit fixes:
- log-noise suppression (skill.yaml / optional-file 'No such file'
  warnings no longer spam Console via the new Result-returning readers)
- diagnostics script now stdin-pipes to sh -s instead of sh -c <script>
  argv, so it runs as one sh process with variable scope preserved
- pill UX: replaced colored dot with state-specific SF Symbol
  (checkmark / stethoscope / arrows / triangle), removed custom
  background, kept .principal placement for centering

Also expanded the 'Known follow-ups' section so users know what's
explicitly deferred post-2.0.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:26:33 -07:00
Alan Wizemann d3055702ef fix: connection pill — revert to .principal, swap dot for state SF Symbol
Rolling back the .primaryAction placement (the pill shifted right and
lost its centered position in the toolbar). The "funny background with
shadow" visible in the toolbar is macOS's own .principal emphasis bezel
— not something Scarf draws, and not something we can cleanly hide
without disabling the toolbar surface itself. The native bezel is the
pill's frame; we just have to make the pill's interior read well inside
it.

Two changes to make the pill itself look like a toolbar tool inside
that bezel:

- Drop the colored dot, replace with a state-specific SF Symbol. The
  icon's shape signals clickability (looks like a tool button), and its
  color signals state (green/orange/yellow/red hierarchical). Less
  "status chip", more "toolbar button with status".
- Icons per state:
  - connected  → checkmark.circle.fill (click to re-probe)
  - degraded   → stethoscope (click to run diagnostics, matches the
                 stethoscope on the Manage Servers row)
  - idle       → arrow.triangle.2.circlepath (checking/retry)
  - error      → exclamationmark.triangle.fill (click for stderr)

Horizontal padding = 4 so the icon-and-label sit balanced inside the
bezel rather than pushed up against its edges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:22:35 -07:00
Alan Wizemann ee1d705abc fix: move connection pill off .principal to drop the emphasis bezel
macOS applies a centered emphasis bezel (light capsule + drop shadow)
to ToolbarItem(placement: .principal) — visible in screenshots as a
doubly-framed "capsule behind the pill" look. The pill itself doesn't
own that background; the toolbar placement does.

.primaryAction (right side of the toolbar) has no decorative
background, so the pill renders as just the colored dot + label text
directly on the toolbar surface. Fits the intended minimal look.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:16:54 -07:00
Alan Wizemann 8e3dafe4c6 fix: remove the pill's own capsule background
The toolbar item already draws its own bezel for the principal-placement
slot; painting a `Color.secondary.opacity(0.08)` capsule on top gave the
pill a doubly-framed look. Drop the pill's background + the padding that
was only there to fit inside the capsule. The dot + label now sit
directly on the toolbar's native surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:14:12 -07:00
Alan Wizemann c51241dc72 fix: diagnostics script — pipe to sh via stdin, not sh -c argv
The previous fix (direct ssh argv, bypassing transport.runProcess) got
us from 0/14 to 7/14, but \$H was empty everywhere it was referenced —
the user's 7/14 report showed:
- probe 4 (hermesHomeConfigured): PASS with empty detail
- probe 5 (hermesDirExists FAIL): "not a directory:" (empty after colon)
- probe 11 (sqlite3CanOpenStateDB FAIL): 'unable to open "/state.db"'

Root cause: `ssh host -- /bin/sh -c <script>` doesn't travel as three
argv entries to the remote. ssh concatenates them with single spaces
into one command string and sends that to the remote's LOGIN shell.
The login shell then runs `$LOGIN_SHELL -c "$string"`, and bash's
parser treats unquoted newlines inside `$string` as command separators.
So the first newline splits the script: `/bin/sh -c H="..."` becomes
one command (which runs in an ephemeral sh subprocess that exits
immediately), and every subsequent line runs in the login shell with
no \$H set.

TestConnectionProbe happens to still work because its downstream lines
don't depend on an assignment from the first line — but the diagnostic
script's \$H is used everywhere, so the entire script is effectively
running with \$H="".

Fix: pipe the script into `/bin/sh -s` on stdin via ssh's own stdin
channel. `sh -s` reads a shell program from stdin and executes it in
one process, variable scope preserved. Implementation uses
Process.standardInput with a Pipe, writing the script after proc.run()
and closing the write end so sh sees EOF. Same as
`cat script.sh | ssh host -- /bin/sh -s` from the command line.

Also: raw-output disclosure panel in the diagnostics sheet now shows
whenever ANY probe fails, not only when all fail. Partial failures are
the most common failure mode and the raw stdout is the only way to see
why a specific detail came back the way it did.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:04:32 -07:00
Alan Wizemann ec03627bcd fix: diagnostics sheet — bypass transport.runProcess for shell script
First-run of diagnostics against a working Mardon returned 0/14 passing
with "(no output)" for every probe — including the trivial "emit
connectivity PASS" that the script emits unconditionally. That meant the
script wasn't executing as written; the parser saw `__END__` but no
probe lines.

Root cause: SSHTransport.runProcess wraps every argument through
`remotePathArg`, which is designed for PATHS (it rewrites `~/` to
`$HOME/` and double-quotes the result with backslash-escapes). Passing
a multi-line shell script with embedded `"$1"` / `"$2"` / `"$3"` and
`printf '\n'` escape sequences through that is corruption — the remote
sh -c receives a scrambled script and silently emits nothing.

TestConnectionProbe already works around this: it builds the ssh argv
directly (ssh host -- /bin/sh -c <script>) so the script travels as a
single opaque argv entry and ssh forwards it to the remote shell
unchanged.

Mirror that approach. RemoteDiagnosticsViewModel.execute now:
- For remote contexts: builds ssh argv directly (ControlMaster-aware,
  uses the same socket as SSHTransport so it's effectively free after
  the first connection), then passes /bin/sh -c <script> as argv.
- For local contexts: spawns /bin/sh -c <script> via Process directly.

Also surfaces raw stdout/stderr/exit-code in a disclosure panel at the
bottom of the sheet, visible only when ALL probes fail. Makes any
future transport-level breakage self-diagnosing: the user sees exactly
what the remote returned, not just "(no output)" rows.

Expose SSHTransport.controlDirPath (already static) as a public helper
so the diagnostics probe reuses the same ControlMaster socket as the
connection itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:56:09 -07:00
Alan Wizemann f8069a4481 fix: don't log 'No such file' as warnings on remote reads
The Result-returning readers I added for the v2.0.1 diagnostics surface
were logging EVERY failure, including routine "file doesn't exist" cases
— e.g. skill.yaml files under ~/.hermes/skills/*/ that are optional
metadata, gateway_state.json before Hermes has started, memories/USER.md
on fresh installs.

In practice this meant the Platforms view and similar feature loaders
that walk directories and read optional files now spam the Console with
warnings on every refresh. That's noisier than useful and actively hides
the signal (permission denied, connection failure, sqlite3 missing) we
added the logging to surface.

readFileDataResult now detects the "no such file" case via either:
- TransportError.fileIO(_, "No such file...") from SSHTransport
- NSCocoaErrorDomain code 260 (NSFileNoSuchFileError) from FileManager
- NSPOSIXErrorDomain code 2 (ENOENT)

and suppresses the warning log for those paths. The Result.failure is
still returned, so any caller that cares (Dashboard's banner, Remote
Diagnostics) can still distinguish missing from present-but-unreadable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:52:28 -07:00
Alan Wizemann 110170d6e9 fix: v2.0.1 — surface remote SSH file-access errors (closes #19)
Three users reported on day-one of v2.0 that SSH connections showed a
green "Connected" pill but every data view read as empty / "not running"
/ "not configured". The common thread across Docker, homelab VM, and
Ubuntu VPS setups: file-access failures on the remote that Scarf
silently swallowed into nil/empty defaults.

Stop swallowing errors
- HermesFileService gains Result-returning variants for the four
  dashboard-critical readers: loadConfigResult, loadGatewayStateResult,
  hermesPIDResult, plus readFileResult / readFileDataResult as
  primitives. Each logs os.Logger warnings on failure. Legacy nil-
  returning signatures remain as thin forwarders.
- HermesDataService.open records lastOpenError with humanized hints
  for the top three failure modes — sqlite3 not installed, permission
  denied, file not found. Each maps to concrete remediation (`apt
  install sqlite3`, "check file perms", "set Hermes data directory").

Dashboard surfaces the error
- DashboardViewModel collects errors from every loader into
  lastReadError, only on remote contexts (local skips the banner).
- DashboardView renders an orange banner above the stats with the
  specific error text, a copy-selectable detail, and a "Run
  Diagnostics…" button.

New Remote Diagnostics sheet (stethoscope icon)
- RemoteDiagnosticsViewModel runs 14 checks in one SSH round-trip via
  a pipe-delimited "KEY|STATUS|DETAIL" protocol. Covers: SSH
  connectivity, remote user/$HOME, Hermes dir existence + readability,
  config.yaml readability + actual read (distinct from just `test -e`
  which can't detect permission issues), state.db readability, sqlite3
  binary presence, sqlite3 open test, hermes binary on non-login AND
  login PATH, pgrep availability.
- Each probe row shows a targeted hint on fail (e.g. "check perms on
  ~/.hermes", "apt install sqlite3", "move PATH export from .bashrc
  to .zshenv"). A Copy Full Report button dumps plain-text output
  for GitHub issues.
- Accessible from Manage Servers (stethoscope button per row) and
  directly from the yellow pill.

Yellow "degraded" connection state
- ConnectionStatusViewModel.Status gains .degraded(reason:) between
  .connected and .error. After tier-1 `true` passes, the probe runs
  tier-2 `test -r $HOME/.hermes/config.yaml` in the same SSH round-
  trip. On tier-2 fail, pill is orange with "Connected — can't read
  Hermes state" tooltip.
- Clicking a degraded pill opens Remote Diagnostics directly. Exactly
  the symptom in #19 is now one click from a specific answer.

Auto-suggest remoteHome for non-default installs
- TestConnectionProbe.TestResult.success gains suggestedRemoteHome:
  String?. When state.db isn't found at the configured path, the
  probe also checks /var/lib/hermes/.hermes, /opt/hermes/.hermes,
  /home/hermes/.hermes, /root/.hermes — the common alternates for
  systemd services, Docker containers, and single-user VPSes — and
  surfaces the first hit as a "Use this" suggestion in Add Server.
- AddServerSheet relabels "Remote ~/.hermes override" to "Hermes data
  directory" with an explanation of when you'd use it.

README
- New "Remote setup requirements" subsection lists the four concrete
  prereqs (SSH, sqlite3, pgrep, read access to ~/.hermes).
- New "Troubleshooting remote connections" paragraph describes the
  diagnostics sheet and remoteHome auto-suggest for the two most
  common failure modes.

Releases
- releases/v2.0.1/RELEASE_NOTES.md for the GitHub release body.
- Ship via `./scripts/release.sh 2.0.1`.

Closes #19.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:40:35 -07:00
Alex Maksimchuk 1293cfa23b fix: use short ControlPath to avoid Unix socket limit on macOS
The ControlMaster socket path ~/Library/Caches/scarf/ssh/%C can
exceed the 104-byte macOS Unix domain socket limit when the
username is long, causing ssh to silently exit 255 with
"unix_listener: path too long for Unix domain socket".

Switch to /tmp/scarf-ssh-<uid> which stays well within the limit.
2026-04-19 23:03:28 -05:00
Alan Wizemann ba8bf14ff0 chore: Bump version to 2.0.0 2026-04-19 13:07:49 -07:00
Alan Wizemann 4212200dca Merge remote-servers into main — v2.0 release
Brings multi-window + multi-server + remote-SSH support to main,
plus the full correctness/UX/concurrency polish pass.

Two commits land:
- 00ca722 feat: multi-window + remote SSH server support (Phases 0-4)
- 5920923 feat: v2.0 — correctness + UX polish on multi-server + remote SSH

See releases/v2.0.0/RELEASE_NOTES.md for the user-facing summary.
2026-04-19 13:07:22 -07:00
Alan Wizemann 5920923d92 feat: v2.0 — correctness + UX polish on multi-server + remote SSH
The multi-window / multi-server / remote-SSH work that landed in
00ca722 (feat: multi-window + remote SSH server support (Phases 0-4))
was feature-complete but accumulated rough edges during dogfooding
against a remote Mac mini. This commit finishes the 2.0 release:
correctness fixes on remote, a chat-view UX overhaul, and a Swift 6
complete-concurrency sweep across the service layer.

Correctness on remote
- Kill the WAL-error spam: snapshotSQLite now runs `PRAGMA
  journal_mode=DELETE` on the remote temp DB before scp, so the
  pulled file is self-contained. Open remote snapshots with
  `file:...?immutable=1` URI as defense-in-depth, and drop the
  pointless `PRAGMA journal_mode=WAL` from HermesDataService.open.
- loadSessionHistory and refreshMessages now force a fresh snapshot
  via refresh(), so resuming a session on a remote shows messages
  persisted since launch (previously stuck on the first snapshot).
- New SnapshotCoordinator actor dedupes concurrent snapshotSQLite
  calls per ServerID — Dashboard + Sessions + Activity no longer
  issue three parallel SSH backups for the same fetch.
- ACP cwd comes from the remote's $HOME (probed once, cached per
  server in UserHomeCache), not the local Mac's NSHomeDirectory().
- Typing into a blank Chat always creates a new session. The old
  auto-resume-most-recent fallback was picking up cron-spawned
  sessions that Hermes had already GC'd, producing silent prompt
  failures.
- handlePromptComplete surfaces non-success stopReasons ("refusal",
  "error", "max_tokens") as a system message so failed prompts no
  longer sit under a forever-spinning "Agent working…".

Chat UX
- Replace six racing onChange-driven scrollTo calls with
  `.defaultScrollAnchor(.bottom)` alone. Manual proxy.scrollTo
  against a LazyVStack that hadn't finished laying out was
  overshooting into whitespace. Layout-pass-integrated anchor
  behaves correctly at stream start and finish.
- Remove ContentUnavailableView swap in RichChatView — it tore down
  the whole ScrollView hierarchy on first message. Empty state now
  lives inside the scroll view.
- continueLastSession surfaces an acpError banner if open() fails,
  instead of silently returning.

Lifecycle hygiene
- ServerRegistry.removeServer closes the server's SSH ControlMaster
  (`ssh -O exit`), prunes its snapshot cache dir, and invalidates
  UserHomeCache for that ID. App launch sweeps orphan snapshot dirs
  whose UUIDs aren't in the registry anymore.
- NSWorkspace.activateFileViewerSelecting (backup-saved-to dialog)
  gated on !context.isRemote; remote surfaces the remote path in the
  saveMessage instead of silently no-op'ing on a nonexistent local
  path.

Swift 6 concurrency — 230 warnings → 1
- Mark ServerContext, HermesPathSet, ServerTransport (protocol),
  LocalTransport, SSHTransport, HermesFileService, and every value-
  type accessor as `nonisolated`. Prevents AppKit-import-driven
  MainActor inference from bleeding onto data-only types.
- Hand-written Codable conformances (vs. synthesized) for
  ACPRequest, ACPRawMessage, ACPError, GatewayState, PlatformState,
  HermesCronJob, CronSchedule, CronJobsFile, AuthFile, AuthEntry.
  Synthesized inits were inferred @MainActor by Swift 6's default-
  isolation rule; hand-written ones are explicitly nonisolated.
- Captured-var refactors in MCPServerEditorViewModel, PluginsView
  Model, LocalTransport.watchPaths. Thread.sleep → Task.sleep in
  TestConnectionProbe.
- Remaining warning is AnyCodable.value mutation in init(from:) —
  Any-typed storage can't be strictly Sendable; acknowledged via
  @unchecked Sendable.

ACP adapter upstream bug (not fixed here, but handled)
- Hermes's ACP adapter returns JSON-RPC success `{"result":{}}` for
  session/load on a missing session, logging the warning only to
  stderr. Scarf can't distinguish "loaded" from "silently missing"
  at that layer; the stopReason=refusal surfacing above catches the
  downstream symptom. Upstream issue worth filing.

Release docs
- releases/v2.0.0/RELEASE_NOTES.md with full user-facing breakdown.
- README.md "What's New" bumped to 2.0 with a multi-server section.
  Compatibility table adds v0.10.0 as verified.
- GitHub repo description updated (via `gh repo edit`) to call out
  multi-server + remote SSH.

35 files changed, +809/-350.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:02:40 -07:00
Alan Wizemann 00ca7229df feat: multi-window + remote SSH server support (Phases 0-4)
Adds the ability to manage multiple Hermes installations — local and
remote over SSH — from the same Scarf app, each in its own window.

Architecture:
- ServerContext value type carries per-server identity + paths through
  every VM and service. ContentView routes serverContext into each
  feature view's init; all 22 routed views thread it through to their
  @State VMs.
- ServerTransport protocol with LocalTransport (FileManager/Process/
  FSEvents) and SSHTransport (system ssh + scp + ControlMaster).
  Services were ported from direct Foundation I/O to transport-routed
  helpers so the same code runs against local or remote.
- WindowGroup(for: ServerID.self) gives each window its own
  AppCoordinator + HermesFileWatcher + ChatViewModel. File menu has
  Open Server commands with keyboard shortcuts (⌘1..⌘9). MenuBarExtra
  fans out per-server with start/stop/restart controls.
- ServerRegistry persists connections to ~/Library/Application
  Support/scarf/servers.json. Add Server sheet probes the remote with
  ssh -v to capture the full handshake on failure.
- Connection-status pill in remote-window toolbars with silent reconnect
  (3s retry on first failure, escalate to red after 2 consecutive),
  known-hosts-mismatch + ssh-add hint cards with copy buttons.

Concurrency / UX hardening (the parts learned the hard way during
dogfooding — captured in the feedback memory):
- ServerContext exposes context.readText / readData / writeText /
  fileExists / runHermes / openInLocalEditor as the canonical I/O
  surface. Every VM uses these; never raw FileManager / Process() /
  NSWorkspace.open with a Hermes path.
- SSHTransport.remotePathArg rewrites ~/foo to "$HOME/foo" so paths
  expand correctly inside the sh -c command we build (POSIX shells
  don't expand ~ inside any quotes).
- Heavy VM load() methods detach to a background task and commit
  results back via MainActor.run, so synchronous ssh round-trips don't
  beach-ball the UI. Applied to Dashboard, Memory, Settings,
  MCPServers, Cron, Plugins, Personalities, QuickCommands, Skills,
  Gateway, Health, CredentialPools.
- LoadingOverlay modifier shows a spinner over empty/stale section
  content during background reloads.
- enrichedShellEnv (zsh -l -i probe, up to 8s) is now warmed at app
  launch off-main so first MainActor caller doesn't block.
- Drop the file watcher's 5s heartbeat — FSEvents covers real changes
  and the heartbeat was triggering wasted reloads across every
  subscribing view.

Chat polish:
- ChatViewModel.hermesBinaryExists is a stored bool probed once at
  init, not a sync transport call evaluated on every body re-render.
- MessageGroupView identifies assistant bubbles by array offset rather
  than message.id, so the streaming → finalized id transition no
  longer destroys + recreates the bubble.
- Static scroll anchor in RichChatMessageList prevents two onChange
  handlers from racing on isWorking flips.

Branch state: feature complete, in active dogfooding. Plan + per-phase
status live at ~/.claude/plans/we-developed-an-application-harmonic-stroustrup.md;
the four hard-won transport/concurrency rules are saved in the
ServerContext-pattern feedback memory for future sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:42:17 -07:00
Alan Wizemann 679dedf132 fix: release script — push main + tag before gh release create
The script was creating the GitHub release before pushing main, which
caused gh to auto-create the v<VERSION> tag at the then-current origin
HEAD (one commit behind the bump, since main hadn't been pushed yet).
The subsequent `git push origin v<VERSION>` was then rejected as
non-fast-forward, leaving the remote tag pointing at the wrong commit.

Caught during v1.6.2. The remote tag for v1.6.2 was force-corrected to
12610fa (the bump commit); the release artifacts themselves were always
correct.

New order: push main → tag main locally → push tag → gh release create.
Gh will now find the tag already on origin and attach to the right
commit. Non-destructive: a retry-safe release can always be resumed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:24:31 -07:00
Alan Wizemann 12610faba0 chore: Bump version to 1.6.2 2026-04-17 17:18:33 -07:00
Alan Wizemann 73b44202ba fix: release script preflight allows pre-written RELEASE_NOTES.md
CLAUDE.md's release-notes convention says "write them to
releases/v<version>/RELEASE_NOTES.md BEFORE running the script" — but
the script's git-clean preflight rejected any working-tree state
including that exact file as untracked. Chicken-and-egg: you couldn't
follow the documented flow.

Preflight now whitelists releases/v<VERSION>/RELEASE_NOTES.md as the one
allowed untracked path. Everything else still fails the check.

Caught while running v1.6.2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:18:23 -07:00
Alan Wizemann eed55cbb0f chore: Ignore release-artifact binaries
Stops the release script's git-clean preflight from tripping on the
zips + appcast-entry.xml that every release run produces under
releases/v<VERSION>/. GitHub Releases hosts the actual downloads; there's
no reason to commit ~30 MB of binaries per release into git history.

RELEASE_NOTES.md stays tracked — it's committed as part of the version
bump by the release script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:16:10 -07:00
Alan Wizemann 14c97bee62 docs: CLAUDE.md — document the release flow + canonical prompts
Adds a Releases section so future Claude sessions (and teammates) don't
have to rediscover the release workflow. Documents:

- The single entry point: `./scripts/release.sh <ver> [--draft]`
- What the script does end-to-end
- The release notes convention (write them before running)
- A handful of canonical prompts the user can type
- Pointers to deeper prerequisite docs (README, script header)

Deliberately brief — detail lives in README and the personal auto-memory
at reference_release_process.md. CLAUDE.md's job here is just to make
the entry point discoverable on session start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:13:15 -07:00
Alan Wizemann 8d3fe70e2c fix: Chat tab false-positive "no credentials" warning before session pick
The orange "No AI provider credentials detected" banner was firing on the
Chat tab whenever no session was selected, even for users whose
credentials were configured and working. The banner only disappeared
when a session started — not because credentials were actually found,
but because the banner's `!hasActiveProcess` gate flipped to false once
ACP launched.

Root cause: `HermesFileService.hasAnyAICredential()` inspected only the
shell environment and `~/.hermes/.env`, while Hermes itself resolves
credentials from two additional places Scarf had never learned about:

  - `~/.hermes/auth.json` — the Credential Pools file written by the
    Configure → Credential Pools UI (the blessed v1.6 flow)
  - `~/.hermes/config.yaml` — embedded `api_key:` under auxiliary.<task>
    and delegation

The preflight now checks all four locations. For auth.json we parse the
JSON and look for any `credential_pool.<provider>[*].access_token` that
is non-empty. For config.yaml we line-scan for `api_key:` leaves with a
non-empty value, matching the defensive style of the existing .env
scanner (no YAML parser needed in a nonisolated function).

Also updated the banner subtitle to point users at Credential Pools
before .env, since the former is the blessed in-app flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:10:51 -07:00
Alan Wizemann da88c98c7a feat: release script builds Universal + ARM64 variants
Each release now produces two distribution zips:
- Scarf-vX.X.X-Universal.zip  (arm64 + x86_64, recommended)
- Scarf-vX.X.X-ARM64.zip      (arm64 only, ~14% smaller)

Both are independently archived, exported with Developer ID, notarized,
and stapled via a new build_variant helper. The appcast still points at
the Universal zip since it works on all supported macs; ARM64 is an
alternative manual download for Apple Silicon users who want the smaller
file.

README updated to list both variants.

Prompted by the v1.6.1 release shipping only Universal; the ARM64 zip
for v1.6.1 was produced ad-hoc and uploaded to the existing release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 20:10:17 -07:00
Alan Wizemann b7ad01f9da chore: Bump version to 1.6.1 2026-04-16 19:04:48 -07:00
Alan Wizemann 868e61979e chore: release script supports --draft + RELEASE_NOTES.md
Drafts skip the appcast push and main tag, so a draft release won't
show up in users' Sparkle update feed and v1.6.0 stays "latest" until
explicitly promoted. The signed appcast entry is saved to the release
dir for later manual promotion.

Also adds release notes file convention: releases/v<VERSION>/RELEASE_NOTES.md
is auto-included in the version-bump commit and used as the GitHub
release body.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 19:04:27 -07:00
Alan Wizemann 9bdd928469 fix: release script — rename scarf.app → Scarf.app after export
Xcode exports the bundle as scarf.app because PRODUCT_NAME = $TARGET_NAME
and the target is lowercase "scarf". Users expect Scarf.app in their
/Applications folder. Renaming the bundle wrapper preserves the
signature (codesign signs contents, not the wrapper directory name).

Caught during a build+sign+verify dry run before the first notarized
release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 18:59:41 -07:00
Alan Wizemann 75e489e39c fix: chat works without a terminal hermes session; surface the real error when it doesn't
A fresh-install user reported Scarf chat only worked while `hermes chat`
was also running in Terminal. ACP connected successfully but sending a
message errored. `~/.hermes/logs/errors.log` showed the real cause:

  RuntimeError: No Anthropic credentials found. Set ANTHROPIC_TOKEN or
  ANTHROPIC_API_KEY, run 'claude setup-token', or authenticate with
  'claude /login'.

The terminal workaround masked the bug because the terminal-launched
`hermes` inherits the user's shell env (ANTHROPIC_* exports, Keychain
session) while a Finder/Dock-launched Scarf subprocess does not.
Scarf's previous PATH-only enrichment (commit b2a29ab) fixed binary
discovery but not credential propagation.

Five changes:

1. Propagate credential env vars from the login shell.
   HermesFileService.enrichedEnvironment() now harvests a conservative
   allowlist of AI-provider keys (ANTHROPIC_API_KEY/TOKEN/BASE_URL,
   OPENAI_*, OPENROUTER_*, GEMINI/GOOGLE/GROQ/MISTRAL/XAI API keys,
   CLAUDE_CODE_OAUTH_TOKEN) alongside PATH. Uses one `zsh` probe with
   null-delimited `printf` so values with newlines survive, cached for
   the process lifetime.

2. Two-attempt shell probe catches nvm/asdf/mise PATH.
   Previous `zsh -l` missed `.zshrc`-exported PATH (nvm). New probe
   first tries `zsh -l -i` (login + interactive, sources .zshrc) with
   prompt frameworks defanged (TERM=dumb, empty PS1/PROMPT,
   POWERLEVEL9K_INSTANT_PROMPT=off, STARSHIP_DISABLE=1,
   ZSH_DISABLE_COMPFIX=true) and a 5s timeout; falls back to `zsh -l`
   with 3s; finally to hardcoded defaults.

3. Resolve `hermes` binary across install locations.
   HermesPaths.hermesBinary is now computed, walking pipx
   (~/.local/bin), Apple Silicon brew (/opt/homebrew/bin), Intel brew
   / manual (/usr/local/bin), and ~/.hermes/bin. Returns the first
   executable match or the pipx default for "Expected at …"
   diagnostics. All 10+ callsites (ACPClient, scarfApp, Health /
   Gateway / Tools / Sessions / QuickCommands / Personalities /
   Settings / WhatsAppSetup / OAuthFlow / CredentialPools
   ViewModels) auto-migrate with zero edits.
   HermesFileService.hermesBinaryPath() shares the same candidate
   list as the source of truth.

4. Surface the real failure in the chat UI.
   ACPClient keeps a 50-line ring buffer of subprocess stderr
   (previously only sent to os_log). New ACPErrorHint.classify pattern-
   matches the common fresh-install failures — "No credentials found",
   "No such file or directory: 'npx'", rate-limit — and returns a short
   human hint. ChatView gains an errorBanner between toolbar and chat
   area showing the hint + raw message + a "Show details" disclosure
   with the stderr tail in a selectable monospaced view, plus a
   clipboard-copy button.

5. Preflight credential check.
   HermesFileService.hasAnyAICredential() scans the enriched env and
   ~/.hermes/.env for any known provider key. ChatViewModel exposes
   `missingCredentials`; the banner becomes a pre-emptive warning
   ("No AI provider credentials detected — add ANTHROPIC_API_KEY to
   ~/.hermes/.env or your shell profile") before the user even hits
   Send. HermesFileWatcher already watches ~/.hermes/.env, so edits
   re-trigger preflight automatically.

Incidental cleanup: recordACPFailure(_:client:context:) folds the
per-site `logger.error` calls, removing three `_ = msg` suppressions.
Dead `enrichedPath` alias removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 18:49:44 -07:00
Alan Wizemann 41ea3aeb83 feat: Sparkle auto-updates + Developer ID notarization pipeline
Adds Sparkle 2 auto-updates and a local release script that produces
signed, notarized, stapled builds for GitHub distribution. App Store
submission was rejected because Scarf spawns the user-installed hermes
binary and reads ~/.hermes/ directly — both forbidden by App Sandbox —
so we commit to the GitHub-release path properly.

- Sparkle SPM dep wired into the app target (link-only; hardened-runtime
  entitlement disable-library-validation lets Sparkle load at runtime).
- Tracked Info.plist with SUFeedURL, SUPublicEDKey, and daily check
  interval; replaces the auto-generated plist so Sparkle keys live in
  version control rather than pbxproj INFOPLIST_KEY_* noise.
- UpdaterService wraps SPUStandardUpdaterController and is injected via
  .environment(). Menu bar, standard app menu (CommandGroup after
  .appInfo), and a new Updates section in Settings → General each call
  updater.checkForUpdates().
- scripts/release.sh runs the full pipeline: version bump → universal
  archive → Developer ID export → notarytool submit (keychain profile
  scarf-notary) → staple → appcast EdDSA sign → gh-pages push → gh
  release → tag. scripts/ExportOptions.plist pins manual Developer ID
  signing for team 3Q6X2L86C4.
- README: removes the right-click-Open workaround (notarized builds
  don't need it), notes Sparkle, adds a Releases section describing
  the pipeline and signing prerequisites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 18:42:20 -07:00
Alan Wizemann eb39dcfa61 docs: Restructure README + add v1.6.0 release binaries
Reorganize the Features section to match the app's sidebar (Monitor, Interact,
Configure, Manage, Project Dashboards, System) so readers find features the
same way they find them in the app. Add a "What's New in 1.6" callout with
links to the release notes.

Binaries: ARM64 (15 MB) and Universal (19 MB). Both signed with the Apple
Development identity (Team 3Q6X2L86C4). Universal contains both arm64 and
x86_64 slices verified with lipo.
2026-04-16 15:51:28 -07:00
Alan Wizemann 93ee194ba0 chore: Bump version to 1.6.0 2026-04-16 15:39:41 -07:00
Alan Wizemann b6d9113579 feat: Settings tabs, Platforms, Credential Pools, Model Picker, and Configure sidebar
Major expansion of Scarf's Hermes platform coverage. Settings is now a 10-tab
layout exposing ~60 previously hidden config fields. A new "Configure" sidebar
section groups per-platform setup, personality management, quick commands,
credential pools, plugins, webhooks, and profile switching.

## Highlights

- **Platforms feature** — Native GUI setup for all 13 messaging platforms
  (Telegram, Discord, Slack, WhatsApp, Signal, Email, Matrix, Mattermost,
  Feishu, iMessage, Home Assistant, Webhook, CLI). Per-platform forms write
  credentials to ~/.hermes/.env and behavior toggles to ~/.hermes/config.yaml.
  WhatsApp and Signal use an inline SwiftTerm terminal for QR/link pairing.

- **Credential Pools** — Provider-aware add/remove with proper type handling.
  OAuth flow uses Process + pipes to extract the authorization URL, open the
  browser explicitly, and accept the code via a form field. Fixes the Anthropic
  OAuth failure where the code had nowhere to be entered.

- **Model Picker** — Hierarchical provider -> model picker backed by
  ~/.hermes/models_dev_cache.json (111 providers, every major model). Used in
  Settings -> General and Delegation. "Custom..." escape hatch for unlisted IDs.

- **Settings as tabs** — 10 tabs (General, Display, Agent, Terminal, Browser,
  Voice, Memory, Aux Models, Security, Advanced). HermesConfig grew from 32 to
  ~90 fields via grouped sub-structs. All new fields round-trip through
  `hermes config set`.

- **Extended existing features** — Cron (create/edit/pause/resume/run-now/
  delete), Skills (Browse Hub + Updates tabs), Health (run `hermes dump` and
  `hermes debug share` with confirmation dialog), Sessions (rename/delete/
  export/export-all).

## Bug fixes

- Tools platform picker showed only CLI (was reading a nonexistent
  `platform_toolsets:` YAML section). Now enumerates KnownPlatforms.all with
  live connectivity dots from gateway_state.json.
- Credentials add with --api-key was triggering OAuth for providers like
  Anthropic because --type was missing. Now always passes --type api-key.
- Remove-by-index used 0-based indexing; hermes CLI expects 1-based. Fixed.
- Various CLI parser fragility issues (plugins, profiles, skills hub, webhooks)
  replaced with structured file reads or proper box-drawn table parsers.

## New core services

- HermesEnvService — reads/writes ~/.hermes/.env atomically, preserves
  comments, commented-out keys get enabled in-place on save, values with
  spaces/specials get quoted, unset commented out (non-destructive).
- ModelCatalogService — decodes the models.dev cache into typed providers and
  models with context/cost/release-date metadata.
- OAuthFlowController — manages the OAuth Process subprocess: extracts the
  auth URL via regex, opens the browser, pipes the code back via stdin,
  detects success/failure markers in output.

## New sidebar structure

Monitor / Projects / Interact / **Configure (new)** / Manage

The Configure section gathers the setup-style features that used to require
the CLI: Platforms, Personalities, Quick Commands, Credential Pools, Plugins,
Webhooks, Profiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 15:39:07 -07:00
Alan Wizemann b2a29ab68d fix: MCP test failures hidden as success; brew/nvm binaries not on PATH
Two related bugs surfaced when testing MCP servers that spawn npx, node,
python, etc. from Homebrew/nvm/asdf/mise installs.

1. MCP test reported success even when the connection failed.
   `hermes mcp test <server>` exits 0 even when the inner connection
   fails — it prints the error to stdout instead. Scarf trusted the
   exit code and rendered a green checkmark while the output said
   "✗ Connection failed: [Errno 2] No such file or directory: 'npx'".
   Fix: also scan output for ✗, "Connection failed", "No such file or
   directory", and "Error:" markers.

2. .app launches start with a minimal PATH that excludes Homebrew.
   When Scarf is launched from Finder/Dock, ProcessInfo's PATH is
   `/usr/bin:/bin:/usr/sbin:/sbin` — no /opt/homebrew/bin, no
   /usr/local/bin, no nvm/asdf/mise shims. Hermes inherits this and
   can't find npx/node/python when spawning MCP server subprocesses.
   Fix: query the user's login shell PATH once via `/bin/zsh -lc 'echo
   $PATH'`, cache it on HermesFileService, and inject it into both
   `runHermesCLI` and the ACP subprocess. Falls back to a sane default
   covering both Apple Silicon and Intel Homebrew if zsh query fails.

Bumps version to 1.5.8 (build 10). Includes signed Universal + ARM64
binaries.
2026-04-16 07:51:32 -07:00
Alan Wizemann 117a0ee9dd fix: MCP Servers — preserve all entries when patching config.yaml
Fixes a bug where adding a second MCP server caused the first to disappear
from the list view, and any args containing YAML reserved characters (e.g.
"@modelcontextprotocol/server-fetch") corrupted the config file.

Three root causes in HermesFileService MCP YAML patching:

1. extractMCPBlock extended through trailing comments to EOF when
   mcp_servers was the last top-level key in config.yaml. Trailing
   comments became part of the "block", so subsequent inserts landed
   at end-of-file rather than inside the entry.

2. patchMCPServerField's entry boundary similarly absorbed trailing
   blanks/comments, making the entry "own" everything until the next
   sibling — or until EOF for the last entry.

3. yamlScalar did not quote values starting with YAML reserved
   indicators (@ * & ? | > ! % , [ ] { } < ` ' "). Args like
   "@modelcontextprotocol/server-fetch" were written bare, producing
   invalid YAML that broke subsequent reads/writes.

Fix: trim trailing blanks/comments off both the block and the entry
in the locator/extractor; quote any scalar starting with a reserved
first character.

Bumps version to 1.5.7 (build 9). Includes signed Universal + ARM64
binaries.

Note: users with an already-corrupted ~/.hermes/config.yaml from the
1.5.6 bug should manually clean up their mcp_servers block (delete the
orphan args at end of file) before upgrading. New writes will be clean.
2026-04-16 07:38:49 -07:00
Alan Wizemann 61d59ba0e4 chore: Re-sign 1.5.6 release binaries
The first 1.5.6 zips contained `linker-signed` bundles with no Sealed
Resources, plus a stray nested scarf.app from a case-insensitive cp.
macOS Gatekeeper rejected the ARM64 download as "damaged"; the
Universal one ran only because the user had already trusted it.

Now both bundles are properly ad-hoc-signed (`Sealed Resources
version=2`) with the hardened runtime preserved. Sizes dropped
significantly (Universal 33MB→16MB, ARM64 27MB→13MB) because the
nested junk is gone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:13:55 -07:00
Alan Wizemann 0a584f6722 chore: Bump version to 1.5.6 and add release binaries
Includes the MCP Servers management UI shipped in 219bca2:
- Add via curated presets (GitHub, Linear, Notion, Sentry, Stripe, …)
  or fully custom (stdio command + args, or HTTP URL with bearer auth)
- Per-server detail view: enable/disable, env + headers editor,
  tool include/exclude filters, resources/prompts toggles, request
  and connect timeouts, OAuth token detection + clearing
- One-click "Test Connection" runs `hermes mcp test` and surfaces
  the discovered tool list
- Gateway-restart banner after config changes that need a reload

README updated with the MCP Servers section, the new MCPServers/
feature module entry, and the `hermes mcp` + `mcp-tokens/` entries
in the Data Sources table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 06:45:19 -07:00
Alan Wizemann 219bca264e feat: Add MCP Servers management UI
Full MCP server lifecycle: add (stdio + HTTP), edit, remove, test,
enable/disable. YAML config patching for args, env, headers, tool
filters, timeouts. OAuth token detection + deletion. Preset picker
for common MCP servers. Gateway restart banner after config changes.

New sidebar section "MCP Servers" under Manage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:53:55 -07:00
Alan Wizemann c7e6a809ed chore: Bump version to 1.5.5 and add release binaries
Ship Hermes v0.9.0 compatibility plus new features (log component
filter, session pill, Fast Mode, Backup/Restore, iMessage, /compress,
Discord threads). README lists both universal and ARM64 downloads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:32:16 -07:00
Alan Wizemann c5d6116f99 feat: Add Hermes v0.9.0 compatibility and new feature surfaces
- Log parser: session-ID tag in v0.9.0 log format is now an optional
  capture group; session pill renders inline and tap-filters the view.
- Logs: component filter (Gateway/Agent/Tools/CLI/Cron) and bounded
  logger column with middle truncation.
- Gateway stop: uses `hermes gateway stop` CLI (v0.9.0's launchctl
  bootout fix) with SIGTERM as fallback.
- HermesConfig: new keys for Fast Mode (service_tier), gateway notify
  interval, force IPv4, context engine, interim assistant messages,
  and Honcho eager init (camelCase per PR #6995).
- Settings: new Performance, Network, Advanced, and Backup & Restore
  sections that call `hermes backup` / `hermes import` off the main
  actor; robust zip-path extraction via regex.
- Platforms: iMessage (BlueBubbles) added to KnownPlatforms and
  icon map.
- Cron: Discord thread delivery (`discord:chat:thread`) renders as
  "Discord thread X in Y".
- Chat: `/compress <focus>` button appears when ACP advertises the
  command; optional focus sheet sends through existing prompt path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:59:46 -07:00
Alan Wizemann 8672ed1e6c chore: Bump version to 1.5.2 and add universal release binary
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:53:21 -04:00
Alan Wizemann 46468890d5 feat: Track ACP token usage, improve chat scroll behavior, and show session costs
Add cumulative token tracking from ACP prompt results with fallback
display when DB has no data yet. Improve scroll-to-bottom reliability
with an external trigger for "Return to Active Session" and onAppear
auto-scroll. Show per-session cost in the dashboard session list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:15:44 -04:00
Alan Wizemann cd503378e2 fix: Move Tools subprocess calls off main thread to fix toggle rendering
Synchronous Process.run()/waitUntilExit() calls on the main thread blocked
SwiftUI's render loop, causing toggle controls to appear as solid blue
rectangles instead of proper switches. All hermes subprocess and file I/O
calls are now async via Task.detached, toggle uses optimistic state update
for immediate visual feedback, and pipe file handles are properly closed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:16:52 -04:00
Alan Wizemann 86762eab6d fix: Harden ACP session stability and recover messages on reconnection
Sessions were silently dying and losing chat history because:
- Pipe write errors (EPIPE) were completely undetected — broken pipe
  writes via Task.detached { handle.write() } failed silently, leaving
  the app unaware the subprocess had crashed
- Reconnection fell back to newSession() when loadSession() failed,
  creating a blank session and permanently losing all conversation context
- No message reconciliation after reconnect — DB-persisted messages
  were never re-fetched, so the UI stayed stale/incomplete
- Keepalive sent bare "\n" which caused json.loads("") parse errors
  in the ACP library every 30 seconds, destabilizing the connection
- TERM=xterm-256color was set on a pipe-based subprocess, risking
  terminal escape sequence pollution in the JSON-RPC stream

Fixes:
- Replace FileHandle.write() with POSIX Darwin.write() + SIGPIPE
  suppression for immediate broken-pipe detection at all write sites
- Send valid JSON-RPC notification {"jsonrpc":"2.0","method":"$/ping"}
  as keepalive instead of bare newlines
- Never fall back to newSession() during reconnection — try
  resumeSession then loadSession, fail visibly if both fail
- Add reconcileWithDB() to merge DB-persisted messages with local
  state after successful reconnection
- Finalize streaming messages immediately on disconnect so partial
  content is preserved before reconnection begins
- Use SIGINT instead of SIGTERM for graceful Python subprocess shutdown
- Remove TERM env var from ACP subprocess environment
- Consolidate disconnect cleanup into single idempotent method
- Add isHandlingDisconnect guard against double-handling
- Increase reconnect attempts from 3 to 5 with capped backoff
- Add "Reconnect" button to toolbar error state

Also: bump version to 1.5.1, set deployment target to macOS 14.6
(Sonoma), and update README with rich chat/ACP features, process
controls, skill editing, and corrected system requirements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:13:34 -04:00
Alan Wizemann a7fd193770 feat: Add ACP real-time chat with stable connection management
Implement a rich chat interface powered by the Hermes ACP (Agent
Communication Protocol) over JSON-RPC stdio pipes, with comprehensive
connection stability:

- ACPClient actor: manages hermes acp subprocess lifecycle, JSON-RPC
  transport, event streaming via AsyncStream, and session management
- ACPMessages: full event parsing for message chunks, thought chunks,
  tool calls, permission requests, and prompt completion
- RichChatViewModel: streaming message display with live updates,
  tool result rendering, and message grouping
- ChatViewModel: ACP session orchestration, auto-start on first
  message, and terminal mode fallback

Connection stability fixes:
- Non-blocking pipe writes via Task.detached to prevent actor deadlock
- Read loop cleanup (handleReadLoopEnded) finishes event stream and
  fails pending requests on EOF instead of hanging silently
- 30s request timeouts on control messages via watchdog Task pattern
- Keepalive: writes \n to stdin every 30s to detect dead processes
  via EPIPE before the next user action
- Health monitor: polls process.isRunning every 5s as belt-and-suspenders
- Auto-reconnect: retries up to 3 times with exponential backoff
  (1s/2s/4s), restores session, only shows error after all retries fail
- connectionLost event displays system message in chat on failure
- Proper stderr pipe management: stored task reference, closed in stop()
- Idempotent cleanup across handleReadLoopEnded, handleTermination,
  and handleConnectionDied via actor serialization and nil guards

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 04:33:03 -04:00
Alan Wizemann 521c6d63fc refactor: Use MarkdownContentView in rich chat bubbles
Replace inline AttributedString(markdown:) in RichMessageBubble with
the shared MarkdownContentView for consistent styled rendering of
headers, lists, blockquotes, and inline formatting in chat messages.
Code blocks continue to use CodeBlockView with its copy button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 00:13:17 -04:00
Alan Wizemann 66d04d838d Merge branch 'chat-interface' into main
Add rich chat interface with iMessage-style message bubbles, terminal
toggle, session info bar, code block rendering with copy button, and
tool call cards. Supports both terminal and rich chat display modes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 00:09:31 -04:00
Alan Wizemann ad30c0a943 feat: Show tool output in Activity inspector (#12)
Add tool result display to the Activity detail pane. When selecting a
tool call, the inspector now shows Arguments → Output → Assistant
Message, giving full visibility into what was requested, what came back,
and how the assistant interpreted it.

- Add fetchToolResult(callId:) query to HermesDataService
- Fetch tool result on entry selection in ActivityViewModel
- Display output in styled monospaced box in detail pane
- Render assistant message with MarkdownContentView

Closes #12

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:52:42 -04:00
Alan Wizemann 44afa8f53b fix: Hide false external memory provider warning on fresh installs
The config.yaml uses YAML empty string literal (provider: '') which the
parser reads as the literal string '' rather than an empty string. Strip
surrounding quotes before checking so '' and "" are treated as empty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:40:35 -04:00
Alan Wizemann 481b937c33 feat: Add rich markdown rendering and skill editing (#11)
Add a custom MarkdownContentView that renders markdown with visual
styling — large headers, styled code blocks with language labels,
bullet and numbered lists, blockquotes with colored borders, and
horizontal rules. YAML frontmatter in skill files is hidden.

Markdown rendering added to:
- Memory view (MEMORY.md, USER.md) with live preview in editor
- Skills view (.md files) with new edit/save capability
- Session messages (assistant responses)
- Dashboard text widgets

Other changes:
- Shared MarkdownRenderer utility for inline formatting
- Split-pane editors (raw markdown left, live preview right)
- saveSkillContent() in HermesFileService with path validation
- Line breaks preserved in non-markdown content (Key: Value format)

Closes #11

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:30:44 -04:00
Alan Wizemann 790efb585b feat: Add Hermes process start/stop/restart controls (#10)
- Add hermesPID() and stopHermes() to HermesFileService for process
  signal management via SIGTERM
- Add process control bar to Health view with running status, PID
  display, and Start/Stop/Restart buttons
- Add Start/Stop/Restart Hermes quick actions to menu bar
- Start launches gateway, stop sends SIGTERM, restart combines both

Closes #10

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 22:48:53 -04:00
Alan Wizemann 3acf95a824 feat: Add Hermes v0.8.0 compatibility and fix Tools tab toggling (#9)
Hermes v0.8.0 support:
- Filter subagent sessions from main list with parent session drill-down
- Add agent.log support (new default log file)
- Add Feishu and Mattermost platforms
- Add Google AI Studio, xAI, Ollama Cloud providers
- Expand cron job model (pre-run scripts, delivery tracking, timeouts, SILENT)
- Add Docker env, command allowlist, and memory profile to config
- Add profile-scoped memory with profile picker
- Add browser backend picker and credential removal to Settings
- Add skills required config warnings
- Consolidate platform icon resolution to single source of truth
- Filter Insights queries to exclude subagent sessions

Bug fix:
- Fix Tools tab phantom toggling when switching platforms (#9)
  - Add .id() to tool list for proper SwiftUI view identity
  - Replace ambiguous plain buttons with segmented Picker

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 22:12:13 -04:00
Alan Wizemann 7d69c82c2b Add rich chat interface with iMessage-style bubbles and terminal toggle
Introduce a new structured chat view as an alternative to the SwiftTerm
terminal. Users can switch between raw terminal and rich chat modes via a
segmented picker in the toolbar. The rich view polls state.db for messages
and renders them as conversation bubbles with markdown, code blocks,
expandable tool call cards, reasoning sections, and a live session info bar
showing tokens, cost, and model. The terminal process stays alive in both
modes — in rich mode it runs hidden while user input from the text field is
piped to its stdin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:33:55 -04:00
Alan Wizemann ae2872e08f Add Hermes v0.7.0 compatibility: reasoning tokens, cost tracking, schema detection
- Auto-detect v0.7.0 database schema with backward compat for older DBs
- Surface reasoning tokens, actual cost, and billing provider from sessions
- Display model reasoning/thinking content in session message bubbles
- Add cost tracking to Dashboard, Insights, and session detail views
- Fix FTS5 search crash on dotted terms (e.g., "config.yaml", "v0.7.0")
- Add missing platforms: Home Assistant, Webhook, Matrix
- Consolidate platform icon mapping into shared KnownPlatforms.icon(for:)
- Map execute_code tool to ToolKind.execute
- Add Settings UI for reasoning effort, approval mode, show cost
- Show memory provider warning when external provider (Honcho) is active
- Replace fragile manual HermesSession init with withTitle() helper
- De-duplicate formatTokens utility function
- Bump version to 1.4.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:46:03 -04:00
Alan Wizemann 303f4502dd Fix version reporting: update MARKETING_VERSION to 1.3.0
All builds were reporting version 1.0 because the Xcode project version
was never updated from its default. Fixes #5, fixes #7.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:50:57 -04:00
Alan Wizemann 815c9dcbcd Merge pull request #6 from awizemann/code-quality
Code quality improvements and webview dashboard widget
2026-04-02 12:04:16 -04:00
Alan Wizemann ef53ac1c93 Replace webview split layout with tabbed Dashboard/Site interface
Dashboards with a webview widget now show a tab bar: Dashboard tab
renders all normal widgets, Site tab displays the web content
full-canvas with even margins. Cleaner UX than the split layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:03:50 -04:00
Alan Wizemann 2a3e8b1422 Add webview widget for embedded web browser in project dashboards
New widget type that renders any URL (local dev servers, HTML reports)
directly in the dashboard via WKWebView. Sections with webviews
automatically split layout: grid widgets left, webview right.
Configurable height, non-persistent data store, navigation error logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 10:29:05 -04:00
Alan Wizemann 563f5a702c Improve code quality: error logging, constants, path validation, safe defaults
- Replace try? with do/catch and [Scarf] error logging in all service-layer
  JSON decoding, file writes, and directory creation
- Extract sqliteTransient constant replacing raw unsafeBitCast(-1, ...) pattern
- Add QueryDefaults and FileSizeUnit enums for all magic numbers
- Guard HOME env var with NSHomeDirectory() fallback instead of force-unwrap
- Add path traversal validation to loadSkillContent()
- Add SessionStats.empty and use it across all initialization sites
- Replace KnownPlatforms array indexing with named .cli constant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 03:15:03 -04:00
Alan Wizemann c7f3ca9be3 Merge feature/project-dashboards into main 2026-04-01 01:30:55 -04:00
Alan Wizemann dbaadb8037 Add Project Dashboards feature with agent-generated widgets
Introduces a new Projects section that renders custom dashboards from
JSON files in project directories. Supports 7 widget types (stat,
progress, text, table, chart, list) with live file-watching refresh.
Includes project registry, SwiftUI Charts integration, schema docs,
and comprehensive README documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:48:13 -04:00
Alan Wizemann ce001fe202 Add left padding to terminal view in chat interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:28:46 -04:00
Alan Wizemann a329eca419 Merge branch 'development' 2026-03-31 15:30:51 -04:00
Alan Wizemann 528de938c5 Add pre-built binary install instructions to README
Universal binary (arm64 + x86_64) available on Releases page.
Updated Building section to Install with download + build options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:30:50 -04:00
Alan Wizemann 4f791d491e Merge pull request #4 from awizemann/development
Config Editor and Voice Fixes
2026-03-31 14:35:33 -04:00
Alan Wizemann dd79891874 Replace read-only Settings with structured config editor
Settings view now has editable form controls organized by section:

Model: editable model name field, provider dropdown picker
Display: personality picker (parsed from config), streaming/reasoning/verbose toggles
Terminal: backend picker (local/docker/singularity/modal/daytona/ssh), max turns stepper
Voice: auto TTS toggle, silence threshold stepper
Memory: enabled toggle, char limit steppers, nudge interval stepper

All changes write via `hermes config set key value` CLI with save
confirmation feedback. Open in Editor button launches the raw YAML
in the default text editor. Paths and raw config sections retained.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:23:56 -04:00
Alan Wizemann a13288e759 Merge branch 'main' of https://github.com/awizemann/scarf 2026-03-31 14:07:44 -04:00
Alan Wizemann a16c8ec2d9 Merge branch 'development' 2026-03-31 14:07:17 -04:00
Alan Wizemann 0e3712116f Merge pull request #3 from awizemann/development
Development to Main - New Voice Commands, Health Dashboard, Tools and gateway control
2026-03-31 14:05:40 -04:00
Alan Wizemann ab45f95790 Fix TTS toggle state reversed on voice enable
Hermes auto-enables TTS when voice mode turns on (auto_tts config).
Our ttsEnabled started as false, so the UI showed off when TTS was
actually on. Now reads auto_tts from config.yaml when voice enables
and sets the initial state to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:03:34 -04:00
Alan Wizemann d31bc63b6a Add microphone permission for voice chat
Hermes voice mode needs mic access when running as a Scarf subprocess.
- Added NSMicrophoneUsageDescription to Info.plist keys
- Created entitlements file with com.apple.security.device.audio-input
- Applied to both Debug and Release configurations

macOS will prompt for mic permission on first push-to-talk use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:56:49 -04:00
Alan Wizemann bc8f4b0c25 Add TTS toggle button to voice controls
Voice toolbar now shows three controls when voice is enabled:
- Mic toggle (voice on/off)
- TTS toggle (speaker icon, sends /voice tts)
- Push to Talk (waveform, sends Ctrl+B)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:50:14 -04:00
Alan Wizemann 55ee99c839 Add Hermes version compatibility section to README
Documents tested versions and the interfaces Scarf depends on
(SQLite schema v6, CLI output parsing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:45:50 -04:00
Alan Wizemann 3477fa733f Redesign Health view with card grid and expandable sections
Replaced the long flat list with a cleaner layout:
- Compact header bar: version, update banner, pass/warn/error counts
- Status/Diagnostics tab switcher (segmented control)
- 2-column card grid: each section is a uniform card showing icon,
  title, and colored status dot counts (green/orange/red)
- Cards have a colored border accent based on worst status
- Click to expand: reveals individual check rows inline
- Only one section expanded at a time for clean scanning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:39:42 -04:00
Alan Wizemann c6f45ac22e Add System Health view with status and diagnostics
New Health section in the Manage group combining hermes status and
hermes doctor output:

- Version header with update available banner (e.g. "47 commits behind")
- Summary badges: passing/warning/issue counts
- Status sections: environment, API keys, auth providers, terminal
  backend, messaging platforms, gateway service, scheduled jobs
- Diagnostics sections: Python environment, required/optional packages,
  config files, directory structure, external tools, API connectivity,
  submodules, tool availability, Skills Hub, Honcho memory
- Each check shows green/orange/red icon with label and detail
- Refresh button to re-run both commands

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:36:56 -04:00
Alan Wizemann b4c93ac79c Add Gateway Control to README features, architecture, and data sources
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:33:16 -04:00
Alan Wizemann c09f167760 Add Gateway Control Center with service control and pairing management
New Gateway section in the Manage group:
- Service controls: Start/Stop/Restart buttons calling hermes gateway CLI
- Status display: state (running/stopped), PID, loaded indicator, stale
  service warning, exit reason, last update timestamp
- Platform cards: each connected messaging platform with connection state
  (reads from gateway_state.json)
- Pairing management: approved users list with revoke button, pending
  pairing codes with approve button
- Auto-refreshes via HermesFileWatcher when gateway state changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:32:29 -04:00
Alan Wizemann b79200e950 Update README with Tools Manager, session management, and revised docs
- Added Tools Manager to features list
- Updated Sessions Browser with rename/delete/export
- Updated Skills Browser with file switcher
- Updated Dashboard with live refresh
- Updated Log Viewer with text search
- Added hermes tools and hermes sessions to data sources table
- Revised How It Works section to cover management actions
- Updated architecture tree with Tools feature

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:02:29 -04:00
Alan Wizemann a800a630a8 Fix session rename not updating across views
After rename:
- Update selectedSession so detail header refreshes immediately
- Update sessionPreviews so previewFor() returns the new title
- Dashboard now observes HermesFileWatcher and reloads on DB changes
- Chat session menu reloads via file watcher (persists across nav)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:01:20 -04:00
Alan Wizemann e4d5bb0364 Add session management: rename, delete, export, and stats bar
Sessions browser enhancements:
- Stats bar: total sessions, messages, DB size, per-platform counts
- Right-click context menu on session rows: Rename, Export, Delete
- Detail view actions menu (ellipsis button): same actions
- Rename: sheet with text field, calls hermes sessions rename
- Delete: confirmation dialog, calls hermes sessions delete --yes
- Export single session: NSSavePanel, calls hermes sessions export
- Export all: button in stats bar, exports everything to JSONL
- Session ID shown in detail header for reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:55:35 -04:00
Alan Wizemann 36757a8c9a Add Tool Management panel with per-platform toggle switches
New Tools section in the Manage group:
- Platform tabs parsed from config.yaml (CLI, Telegram, Discord, etc.)
- Lists all toolsets with emoji icon, name, description, and toggle
- Toggle switches call hermes tools enable/disable under the hood
- Shows enabled count vs total
- MCP server status section at bottom
- Optimistic UI update on toggle with CLI fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:47:25 -04:00
Alan Wizemann cfbf3ea142 Update README with Insights, voice controls, and activity filter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:31:50 -04:00
Alan Wizemann f3cb1eb86b Add Insights Dashboard with usage analytics
New sidebar section showing rich analytics from the sessions database:
- Overview grid: sessions, messages, tokens (input/output/cache), active
  time, avg session duration, avg messages per session
- Model breakdown: sessions and total tokens per model
- Platform breakdown: CLI vs Telegram etc with session/message counts
- Top tools bar chart: ranked by call count with percentages
- Activity patterns: day-of-week bars and hourly heatmap
- Notable sessions: longest, most messages, most tokens, most tool calls
  with clickable links to open in Sessions browser
- Time period selector: 7/30/90 days or all time

Also adds ROADMAP.md documenting the full feature expansion plan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:45:35 -04:00
Alan Wizemann 2b57025f3c Merge pull request #1 from awizemann/development
Buy Me a Coffee (seriously, I am tired).
2026-03-31 03:44:49 -04:00
Alan Wizemann 2a14e28589 Fix Buy Me a Coffee button image URL
Use CDN-hosted default button instead of API-generated image.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 03:27:43 -04:00
Alan Wizemann 39bac7d2be Add Buy Me a Coffee button to README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 03:26:12 -04:00
Alan Wizemann af8e120c9f Remove cost stat card from dashboard
Hermes cost tracking returns $0.00 for models not in its static
pricing table (including claude-haiku-4-5). Token counts remain
displayed since those are always accurate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 03:12:04 -04:00
Alan Wizemann 0d38856b3e Add session filter to Activity view
Dropdown in the filter bar lets users scope activity to a single
session or view all. Sessions are labeled with their first user
message preview. Combines with the existing tool-kind filter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 03:03:28 -04:00
Alan Wizemann 0a73aab825 Show session previews instead of raw IDs everywhere
Fetches the first user message per session from the database and
uses it as the display label. Falls back to session title, then ID.

Applied consistently across:
- Chat session resume menu (shows what each session was about)
- Sessions browser list and detail header
- Dashboard recent sessions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 02:58:55 -04:00
Alan Wizemann 0344ce2b98 Persist chat terminal across navigation, add session resume
The terminal process now survives sidebar navigation by hoisting
ChatViewModel to the app level via environment injection. The
LocalProcessTerminalView instance lives in the view model rather
than being recreated by NSViewRepresentable each time.

Added session management:
- Session menu with New Session, Continue Last, and Resume by ID
- Recent sessions list pulled from the database
- Hermes --resume and --continue flags for session continuity
- Status indicator showing active/inactive process state
- Empty state prompting user to start or resume a session

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 02:53:47 -04:00
Alan Wizemann eb38ee343c Fix Activity Feed description: recent, not real-time
The activity feed loads historical tool calls from the database,
it does not stream live events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 02:39:47 -04:00
Alan Wizemann eacdf2bf4b Update Xcode project: app display name, icon assets, and Utilities category
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 02:36:57 -04:00
Alan Wizemann 57cd05ce73 Add app icon to README and commit icon assets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 02:35:35 -04:00
Alan Wizemann 76b3730245 Fix clone URL in README to point to actual repo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 02:33:45 -04:00
Alan Wizemann 18278a3357 Initial release: Scarf — macOS GUI for the Hermes AI agent
Native SwiftUI app providing full visibility into the Hermes AI agent:
- Dashboard with system health, token usage, and cost tracking
- Sessions browser with conversation detail and FTS5 search
- Activity feed with tool call inspector (read/edit/execute/fetch/browser)
- Embedded terminal chat via SwiftTerm with full ANSI/Rich rendering
- Memory viewer/editor with live file-watching refresh
- Skills browser by category with file content viewer
- Cron job viewer with output display
- Real-time log tailing with level filtering
- Settings display with raw config and Finder path links
- Menu bar status icon with quick actions

Architecture: MVVM-Feature, zero dependencies beyond SwiftTerm,
read-only SQLite access, Swift 6 strict concurrency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 02:30:04 -04:00
320 changed files with 65969 additions and 3593 deletions
+28
View File
@@ -0,0 +1,28 @@
---
name: Bug Report
about: Report a bug in Scarf
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '...'
3. See error
**Expected behavior**
What you expected to happen.
**Screenshots**
If applicable, add screenshots.
**Environment**
- macOS version:
- Xcode version:
- Hermes version:
- Scarf version/commit:
+19
View File
@@ -0,0 +1,19 @@
---
name: Feature Request
about: Suggest an idea for Scarf
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem?**
A clear description of the problem.
**Describe the solution you'd like**
What you want to happen.
**Describe alternatives you've considered**
Other solutions you've thought about.
**Additional context**
Any other context, screenshots, or mockups.
@@ -0,0 +1,42 @@
<!--
Use this template when submitting a new Scarf project template or updating
an existing one. For regular code/docs PRs, delete this template and write
your own summary.
Switch to this template by adding `?template=template-submission.md` to the
compare URL, or let GitHub pick it up automatically when you touch files
under templates/.
-->
## What's in this PR
- [ ] New template: `templates/<your-handle>/<your-template-name>/`
- [ ] Update to existing template: `templates/<author>/<name>/` (which one and why)
## One-line pitch
_What does this template do for its installers? Two sentences max._
## Checklist
- [ ] I wrote this template, or have the author's explicit permission to submit it.
- [ ] `AGENTS.md` is present and tells any cross-agent what the project does and how to run it.
- [ ] `README.md` includes install, customize, and uninstall instructions.
- [ ] The bundle's `template.json` `contents` claim matches what's actually in the zip.
- [ ] Cron jobs (if any) ship paused and use self-contained prompts.
- [ ] No secrets in any file (API keys, tokens, hostnames, IPs, credentials).
- [ ] No writes to `config.yaml`, `auth.json`, or credential paths — v1 installer will refuse.
- [ ] `python3 tools/build-catalog.py --check` passes locally.
- [ ] I installed + uninstalled this template on my machine and verified the `AGENTS.md` contract works end-to-end.
- [ ] I did **not** edit `templates/catalog.json` — the maintainer regenerates it post-merge.
## Testing notes
_What did you run, what did you see? Paste the log output of the cron job
firing once, or the chat transcript of asking the agent to do the main
thing. Reviewers don't have your machine — show, don't tell._
## Screenshots (optional)
_Drop screenshots of the installed dashboard, or the catalog detail page
rendered locally (`./scripts/catalog.sh preview && open /tmp/scarf-catalog-preview/templates/<slug>/index.html`)._
@@ -0,0 +1,74 @@
# Validates `.scarftemplate` bundles on PRs that touch templates/.
#
# Mirrors the invariants `ProjectTemplateService.verifyClaims` enforces at
# install time. Runs the same Python script the maintainer uses locally
# (tools/build-catalog.py --check) so a bundle can't reach main unless the
# validator is happy.
#
# Also runs tools/test_build_catalog.py so drift between the validator and
# its own test suite is caught on the same PR.
name: Validate template submissions
on:
pull_request:
paths:
- 'templates/**'
- 'tools/build-catalog.py'
- 'tools/test_build_catalog.py'
- '.github/workflows/validate-template-pr.yml'
permissions:
contents: read
pull-requests: write
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full clone so we can diff against the PR base and scope
# --only to just the changed templates if we want to later.
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
# The validator is stdlib-only and tested against 3.9+ (the
# system Python on current macOS, what most maintainers run
# locally). CI uses 3.11 for faster cold-cache times on
# GitHub Actions runners — same stdlib APIs, same code paths.
python-version: '3.11'
- name: Run validator unit tests
run: python3 tools/test_build_catalog.py -v
- name: Validate every template
id: validate
run: |
set -o pipefail
python3 tools/build-catalog.py --check 2>&1 | tee /tmp/validator.log
- name: Post failure comment
if: failure() && steps.validate.outcome == 'failure'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let body = '## Template validation failed\n\n';
try {
const log = fs.readFileSync('/tmp/validator.log', 'utf8');
body += '```\n' + log.slice(-3000) + '\n```\n';
} catch (e) {
body += 'See the failed job log for details.\n';
}
body += '\nFix the issues above and push again — the check reruns automatically.\n';
body += '\nLocal reproduction: `python3 tools/build-catalog.py --check`\n';
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});
+58
View File
@@ -0,0 +1,58 @@
# Xcode
build/
.gh-pages-worktree/
.wiki-worktree/
DerivedData/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
*.xccheckout
*.moved-aside
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
# Swift Package Manager
.build/
Packages/
Package.pins
Package.resolved
*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/
.swiftpm/
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
# IDE
*.swp
*.swo
*~
.idea/
.vscode/
# Claude Code
.claude/
scarf/standards/backups/
# Scarf project dashboards (user-specific)
.scarf/
# Release artifacts — GitHub Releases hosts the binaries; no need to bloat git
# history. RELEASE_NOTES.md stays tracked (committed with the version bump).
releases/v*/*.zip
releases/v*/appcast-entry.xml
# Wiki helper: personal patterns (hostnames, IPs) blocked from the wiki push.
scripts/wiki-blocklist.txt
+129
View File
@@ -0,0 +1,129 @@
# Scarf — macOS GUI for the Hermes AI Agent
## Project Structure
```
scarf/scarf/ Xcode project root (PBXFileSystemSynchronizedRootGroup — auto-discovers files)
scarf/ Main app target source
Core/Services/ HermesDataService, HermesFileService, HermesLogService, ACPClient, HermesFileWatcher
Core/Models/ Plain structs: HermesSession, HermesMessage, HermesConfig, etc.
Features/ MVVM-F feature modules (Dashboard, Sessions, Activity, Chat, Memory, Skills, Cron, Logs, Settings)
Navigation/ AppCoordinator, SidebarView
docs/ PRD, Architecture, Discovery notes
standards/ Copied development standards (read-only reference)
```
## Architecture Rules
- **MVVM-F**: Features never import sibling features. Cross-feature goes through services.
- **AppCoordinator**: Single `@Observable` coordinator for all navigation state, injected via `.environment()`.
- **No external dependencies**: System SQLite3, Foundation JSON, AttributedString markdown.
- **Read-only DB access**: Never write to `~/.hermes/state.db`. Only write to memory files and cron jobs.
- **Sandbox disabled**: App reads `~/.hermes/` directly.
- **Swift 6 concurrency**: `@MainActor` default. Services use `nonisolated` + async/await.
## Key Paths
- Hermes home: `~/.hermes/`
- SQLite DB: `~/.hermes/state.db` (WAL mode, read-only)
- Config: `~/.hermes/config.yaml`
- Memory: `~/.hermes/memories/MEMORY.md`, `~/.hermes/memories/USER.md`
- Sessions: `~/.hermes/sessions/session_*.json`
- Cron: `~/.hermes/cron/jobs.json`
- Logs: `~/.hermes/logs/errors.log`, `~/.hermes/logs/gateway.log`
- ACP: `hermes acp` subprocess (stdio JSON-RPC)
## Build
```bash
xcodebuild -project scarf/scarf.xcodeproj -scheme scarf -configuration Debug build
```
## Releases
Shipped via a single local script. **Never run manual `xcodebuild archive` / `notarytool` / `gh release create` steps — use the script so nothing is skipped or misordered.**
```bash
./scripts/release.sh <version> # full release: notarize → appcast → gh-pages → tag
./scripts/release.sh <version> --draft # draft: everything builds + notarizes, but appcast/tag are skipped
```
The script bumps version, archives Universal (arm64 + x86_64) + ARM64-only variants, signs with Developer ID, notarizes via `xcrun notarytool` (keychain profile `scarf-notary`), staples, EdDSA-signs the appcast entry with Sparkle's key, pushes the appcast to `gh-pages`, and creates a GitHub release with both zips attached. Draft mode stops after the release is uploaded so the current version stays "latest" until explicitly promoted.
**Release notes convention:** write them to `releases/v<version>/RELEASE_NOTES.md` BEFORE running the script — it's auto-included in the version-bump commit and used as the GitHub release body. If absent, a placeholder is used.
**Canonical prompts (any of these trigger the flow):**
- "Release v1.6.2" — full release
- "Release v1.6.2 as draft" — draft mode
- "Prepare v1.6.2 release notes from recent commits, then release" — generate notes first, then run
**Prerequisites (one-time, already set up on Alan's machine):** Developer ID Application cert in login Keychain (team `3Q6X2L86C4`), notarytool keychain profile `scarf-notary`, Sparkle EdDSA private key in Keychain item `https://sparkle-project.org`, `gh-pages` branch + GitHub Pages enabled. See the header of [scripts/release.sh](scripts/release.sh) and the Releases section in [README.md](README.md) for details.
## Wiki
Public documentation lives in the GitHub wiki at https://github.com/awizemann/scarf/wiki. The wiki is a separate git repo cloned to `.wiki-worktree/` in the repo root (gitignored, sibling to `.gh-pages-worktree/`). Internal dev notes stay in `scarf/docs/`; the wiki is for public-facing reference.
**Update the wiki when:**
- A new feature module is added under `scarf/scarf/scarf/Features/` → extend the relevant User Guide page.
- A new core service is added under `Core/Services/` → extend `Core-Services.md`.
- Architecture changes (AppCoordinator, transport, MVVM-F rule, sandbox) → `Architecture-Overview.md` + the specific sub-page.
- Hermes version bumps in this file → `Hermes-Version-Compatibility.md`.
- `scripts/release.sh` completes a full (non-draft) release → bump latest-version on `Home.md` + append to `Release-Notes-Index.md`.
- Keyboard shortcut or sidebar section changes → `Keyboard-Shortcuts.md` / `Sidebar-and-Navigation.md`.
**Skip for:** bug fixes with no user-observable change, pure refactors, typos, test-only changes, internal cleanups.
```bash
./scripts/wiki.sh pull # always first
# edit .wiki-worktree/*.md with normal tools
./scripts/wiki.sh commit "docs: describe X" # runs secret-scan
./scripts/wiki.sh push # runs secret-scan again, then push
```
**Never** commit API keys, tokens, `.env` files, private keys, or real hostnames/IPs to the wiki. The script's two-pass secret-scan blocks common token patterns and a user-maintained blocklist at `scripts/wiki-blocklist.txt` (gitignored). Do not bypass without explicit approval. Full workflow on the wiki itself at `.wiki-worktree/Wiki-Maintenance.md`.
## Hermes Version
Targets Hermes v0.9.0 (v2026.4.13). Log lines may carry an optional `[session_id]` tag between the level and logger name — `HermesLogService.parseLine` treats the session tag as an optional capture group, so older untagged lines still parse.
## Project Templates
Scarf ships a `.scarftemplate` format (v1 as of 2.2.0) for sharing pre-packaged projects across users and machines. A bundle is a zip containing:
- `template.json` — manifest (id, name, version, `contents` claim)
- `README.md` — shown in the install preview sheet
- `AGENTS.md` — required; the [Linux Foundation cross-agent instructions standard](https://agents.md/) — every template is agent-portable out of the box
- `dashboard.json` — copied to `<project>/.scarf/dashboard.json`
- `instructions/…` — optional per-agent shims (`CLAUDE.md`, `GEMINI.md`, `.cursorrules`, `.github/copilot-instructions.md`)
- `skills/<name>/…` — optional; installed to `~/.hermes/skills/templates/<slug>/` (namespaced so uninstall is `rm -rf` on one folder)
- `cron/jobs.json` — optional; registered via `hermes cron create` with a `[tmpl:<id>] …` name prefix and immediately paused
- `memory/append.md` — optional; appended to `~/.hermes/memories/MEMORY.md` between `<!-- scarf-template:<id>:begin/end -->` markers
Key services: [ProjectTemplateService.swift](scarf/scarf/Core/Services/ProjectTemplateService.swift) (inspect + validate + plan), [ProjectTemplateInstaller.swift](scarf/scarf/Core/Services/ProjectTemplateInstaller.swift) (execute a plan), [ProjectTemplateExporter.swift](scarf/scarf/Core/Services/ProjectTemplateExporter.swift) (build a bundle from a project), [ProjectTemplateUninstaller.swift](scarf/scarf/Core/Services/ProjectTemplateUninstaller.swift) (reverse an install using the lock file). UI in [Features/Templates/](scarf/scarf/Features/Templates/). The `scarf://install?url=<https URL>` deep link + `file://` URLs for `.scarftemplate` files are handled by [TemplateURLRouter.swift](scarf/scarf/Core/Services/TemplateURLRouter.swift) and `onOpenURL` in `scarfApp.swift`. A `<project>/.scarf/template.lock.json` uninstall manifest is written after every install and drives the uninstall flow.
**Uninstall semantics:** driven by the lock file. Only files listed in `lock.projectFiles` are removed from the project dir; user-added files (e.g. a `sites.txt` created on first run) are preserved. If every file in the dir was installed by the template, the dir is removed too; otherwise the dir stays with just the user's files. Skills namespace is always removed wholesale (it's isolated). Cron jobs are removed via `hermes cron remove <id>` after resolving each lock-recorded name. Memory block is stripped between the `begin`/`end` markers, leaving the rest of MEMORY.md intact. No "undo" — uninstall is destructive; to re-install, run the install flow again. Uninstall UI lives on the project-list context menu and the dashboard header (only shown when the selected project has a lock file).
**Never** let a template write to `config.yaml`, `auth.json`, sessions, or any credential path — the v1 installer refuses. If you extend the format, treat the preview sheet as load-bearing: the user's only trust boundary is that the sheet is honest about everything that's about to be written.
## Template Catalog
Shipped community templates live at `templates/<author>/<name>/` (one level down — `templates/CONTRIBUTING.md` explains the submission flow for authors). The catalog site is generated from this directory and served at `awizemann.github.io/scarf/templates/` alongside the Sparkle appcast — the two coexist on the `gh-pages` branch but touch completely disjoint paths.
Pipeline:
- **Validator + regenerator:** [tools/build-catalog.py](tools/build-catalog.py) is stdlib-only Python (3.9+). It walks `templates/*/*/`, validates every `.scarftemplate` against its manifest claim (mirrors the Swift `ProjectTemplateService.verifyClaims` invariants), enforces a 5 MB bundle-size cap, scans for high-confidence secret patterns, checks `staging/` matches the built bundle byte-for-byte, and emits `templates/catalog.json`. Tested by [tools/test_build_catalog.py](tools/test_build_catalog.py) — 16 tests covering every validation path.
- **Wrapper:** [scripts/catalog.sh](scripts/catalog.sh) mirrors the `scripts/wiki.sh` shape with `check / build / preview / serve / publish` subcommands. `publish` runs a second-pass secret-scan against the rendered site before committing + pushing `gh-pages`.
- **Site source:** `site/index.html.tmpl` + `site/template.html.tmpl` are `{{TOKEN}}`-substitution templates. `site/widgets.js` (~300 lines of vanilla JS) is the dogfood — renders a `ProjectDashboard` JSON into HTML using the same widget vocabulary the Swift app uses, so each template's detail page shows a live preview of its post-install dashboard.
- **Install-URL hosting:** raw-served from `main` at `https://raw.githubusercontent.com/awizemann/scarf/main/templates/<author>/<name>/<name>.scarftemplate`. No per-template Releases ceremony.
- **CI gate:** [.github/workflows/validate-template-pr.yml](.github/workflows/validate-template-pr.yml) runs the Python validator + its own test suite on every PR that touches `templates/`, the validator, or its tests. Failures post a comment on the PR with the last 3 KB of the validator log.
Maintainer workflow on merge to main:
```bash
./scripts/catalog.sh build # regenerate templates/catalog.json + .gh-pages-worktree/templates/
./scripts/catalog.sh publish # secret-scan rendered output + commit + push gh-pages
```
Same cadence as `scripts/release.sh` (manual, auditable, no auto-deploy). Runs stay isolated: release.sh only touches `appcast.xml` on gh-pages; catalog.sh only touches `templates/` on gh-pages. Never push catalog output on a release cadence or vice versa.
**Schema is Swift-primary.** When `ProjectDashboardWidget.type` gains a new case or `ProjectTemplateManifest` adds a field, update Swift first, then mirror into `tools/build-catalog.py` (`SUPPORTED_WIDGET_TYPES`, `_validate_manifest`, `_validate_contents_claim`) so the web validator stays honest. The Python test suite's real-bundle test catches drift on the example template but not on the full widget vocabulary — add a synthetic fixture to `test_build_catalog.py` for any new widget type.
+70
View File
@@ -0,0 +1,70 @@
# Contributing to Scarf
Thanks for your interest in contributing to Scarf.
## Getting Started
1. Fork and clone the repo
2. Open `scarf/scarf.xcodeproj` in Xcode 26.3+
3. Build and run (requires macOS 26.2+ and Hermes installed at `~/.hermes/`)
## Architecture
Scarf uses the MVVM-Feature pattern. Each feature is a self-contained module under `Features/`:
```
Features/FeatureName/
Views/ SwiftUI views
ViewModels/ @Observable view models
```
Rules:
- Features never import sibling features directly
- Cross-feature navigation goes through `AppCoordinator`
- Services in `Core/Services/` are shared across features
- Models in `Core/Models/` are plain structs
## Guidelines
- Keep it simple. Minimal dependencies, no over-engineering.
- No commented-out code, TODOs, or deferred functionality in PRs.
- All code must build with zero warnings.
- Follow existing patterns — look at how similar features are built before adding new ones.
- The app only reads from `~/.hermes/state.db` (never writes). Memory files are the exception.
- Swift 6 strict concurrency: `@MainActor` default isolation, `nonisolated` for service methods.
## Documentation
Public docs live in the [GitHub wiki](https://github.com/awizemann/scarf/wiki). Small fixes (typos, clarifications) can be made via the "Edit" button on any wiki page — you need push access to the main repo. For larger changes, clone the wiki locally (`git clone git@github.com:awizemann/scarf.wiki.git`) or open an issue describing the proposed change.
## Adding a Language
Scarf ships with English + Simplified Chinese, German, French, Spanish, Japanese, and Brazilian Portuguese. To add another locale (or improve an existing one):
1. **Fork** the repo and create a branch.
2. **Add the locale to `knownRegions`** in `scarf/scarf.xcodeproj/project.pbxproj` — follow the existing list (e.g. add `it` after `"pt-BR"`).
3. **Drop a new JSON file at `tools/translations/<locale>.json`** — copy an existing one (say `tools/translations/es.json`) as a starting point. Each entry maps the English source string to your translation. Keys you omit fall back to English at runtime — do that for proper nouns (Scarf, Hermes, Anthropic, OAuth, SSH, …) and for anything technical that shouldn't translate.
4. **Preserve format specifiers exactly**: `%@`, `%lld`, `%d`, positional `%1$@` / `%2$lld`, etc. If word order needs to change in your language, use positional forms (`%1$@ … %2$@`).
5. **Add your locale to `tools/merge-translations.py`'s `LOCALES` list** and run `python3 tools/merge-translations.py` — this writes your translations into `scarf/scarf/Localizable.xcstrings`.
6. **Translate `scarf/scarf/InfoPlist.xcstrings`** (the macOS microphone-permission prompt) for your locale. Add a new `stringUnit` under `localizations`.
7. **Build** (`xcodebuild -project scarf/scarf.xcodeproj -scheme scarf build`) and **sanity-check in Xcode**: Scheme → Run → App Language → your locale. Walk the main views (Dashboard, Chat, Settings) and look for clipping or obvious leaks.
8. **Open a PR** including the new JSON file, the updated catalog, and the pbxproj / script changes. Mention which routes you spot-checked.
AI translation is fine for the first pass — it's how the initial six locales landed. Native-speaker review improves quality and is always welcome, either as a follow-up PR or as review comments on the initial one.
See [scarf/docs/I18N.md](scarf/docs/I18N.md) for deeper context on the String Catalog setup and which strings are intentionally kept verbatim.
## Reporting Issues
Open an issue with:
- What you expected to happen
- What actually happened
- macOS version and Hermes version
- Steps to reproduce
## Pull Requests
- Open an issue first to discuss the change
- One feature or fix per PR
- Include a clear description of what changed and why
- Ensure the project builds with `xcodebuild -project scarf/scarf.xcodeproj -scheme scarf build`
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Alan Wizemann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+428
View File
@@ -0,0 +1,428 @@
<p align="center">
<img src="icon.png" width="128" height="128" alt="Scarf app icon">
</p>
<h1 align="center">Scarf</h1>
<p align="center">
A native macOS companion app for the <a href="https://github.com/hermes-ai/hermes-agent">Hermes AI agent</a>.<br>
Full visibility into what Hermes is doing, when, and what it creates.
</p>
<p align="center">
<img src="https://img.shields.io/badge/macOS-14.6+%20Sonoma-blue" alt="macOS">
<img src="https://img.shields.io/badge/Swift-6-orange" alt="Swift">
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
<br>
<em>Available in English, 简体中文, Deutsch, Français, Español, 日本語, and Português (Brasil).</em>
<br><br>
<a href="https://www.buymeacoffee.com/awizemann"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" height="28"></a>
</p>
## What's New in 2.2
- **Project Templates** — Scarf projects can now travel. Package a project's dashboard, agent instructions, skills, and cron jobs into a `.scarftemplate` bundle, hand it to anyone, and they install it in one click. Every bundle ships with a cross-agent `AGENTS.md` ([agents.md](https://agents.md/) standard) so the instructions work in Claude Code, Cursor, Codex, Aider, and the 20+ other agents that read it natively. Browser-based one-click install via `scarf://install?url=…` deep links. Export / Install from File / Install from URL live under the new **Templates** menu in the Projects toolbar.
- **Preview-before-apply** — Every install shows a preview sheet listing the exact project directory that will be created, every file inside it, every skill that will be namespaced, every cron job that will be registered (paused by default), and a live diff of any memory appendix. Nothing writes until you click Install.
- **Safe-by-design** — Skills install into `~/.hermes/skills/templates/<slug>/` so they never collide with your own. Cron jobs carry a `[tmpl:<id>]` tag and start paused. A `template.lock.json` records everything written for easy uninstall. Templates **never** touch `config.yaml`, `auth.json`, sessions, or credentials.
See the full [v2.2.0 release notes](https://github.com/awizemann/scarf/releases/tag/v2.2.0).
### Previously, in 2.1
- **Seven languages** — Full UI translations for Simplified Chinese, German, French, Spanish, Japanese, and Brazilian Portuguese on top of English. Scarf respects the system language by default; override per-app via **System Settings → Language & Region → Apps → Scarf**. Contributor workflow for adding more locales is documented in [CONTRIBUTING.md → Adding a Language](CONTRIBUTING.md#adding-a-language).
- **Locale-aware number formatting** — Currency, byte sizes, compact token counts (`15K`, `1.5M`), and day-of-week charts now follow the user's locale instead of POSIX / English defaults.
- **Chat slash-command menu** — Type `/` in Rich Chat to browse every command the agent has advertised plus any user-defined `quick_commands:` from config.yaml. ↑/↓ to navigate, Tab/Enter to complete, Esc to dismiss.
- **Chat polish** — Auto-scroll on send and on prompt completion, a non-blocking loading spinner during session reconnects, properly centered empty state, and the long-standing "session loads with whitespace" bug fixed (LazyVStack → VStack in the message list).
See the full [v2.1.0 release notes](https://github.com/awizemann/scarf/releases/tag/v2.1.0).
### Previously, in 2.0
- **Multi-server** — Manage multiple Hermes installations (local + any number of remotes) from one app. Each window binds to one server; open them side-by-side.
- **Remote Hermes over SSH** — Every feature that worked against your local `~/.hermes/` now works against a remote host. File I/O routes through `scp`/`sftp`; chat ACP runs over `ssh -T`; SQLite is served from atomic `.backup` snapshots pulled on file-watcher ticks.
- **Chat UX overhaul** — No more white-screen flash on first message, no more scroll jumping into whitespace during streaming, failed prompts explain themselves instead of silently spinning forever.
- **Correctness pass** — Fixed remote WAL error spam, stale-snapshot session resume, auto-resume of dead cron sessions, 230+ Swift 6 concurrency warnings.
See the [v2.0.0 release notes](https://github.com/awizemann/scarf/releases/tag/v2.0.0) for the full 2.0 series.
### Previously, in 1.6
- **Platforms** — Native GUI setup for all 13 messaging platforms, no more hand-editing `.env`
- **Credential Pools** — Fixed OAuth flow and API-key handling; pick providers from a catalog
- **Model Picker** — Hierarchical browser backed by the 111-provider models.dev cache
- **Settings tabs** — 10 organized tabs covering ~60 previously hidden config fields
- **Configure sidebar** — Personalities, Quick Commands, Plugins, Webhooks, Profiles
See the [v1.6.0 release notes](https://github.com/awizemann/scarf/releases/tag/v1.6.0) for the full 1.6 series.
## Multi-server, one window per server
Scarf 2.0 is a multi-window app. Each window is bound to exactly one Hermes server — your local `~/.hermes/` is synthesized automatically, and you can add remotes via **File → Open Server…****Add Server** (host, user, port, optional identity file). Open a second window for a different server and the two run side-by-side with independent state.
Remote Hermes is reached over system SSH — the same `~/.ssh/config`, ssh-agent, ProxyJump, and ControlMaster pooling your terminal uses. File I/O flows through `scp`/`sftp`; SQLite is served from atomic `sqlite3 .backup` snapshots cached under `~/Library/Caches/scarf/snapshots/<server-id>/`; chat (ACP) tunnels as `ssh -T host -- hermes acp` with JSON-RPC over stdio end-to-end. Everything in the feature list below works against remote identically to local.
### Remote setup requirements
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 on the remote 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 "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`).
### Troubleshooting remote connections
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. Open **Manage Servers → 🩺 Run Diagnostics** (or click the yellow "Can't read Hermes state" pill in the toolbar). The diagnostics sheet runs fourteen checks in one SSH session — connectivity, `sqlite3` presence, read access to `config.yaml` and `state.db`, the effective non-login `$PATH` — and tells you exactly which one fails and why, with remediation hints for each. Use the **Copy Full Report** button to paste the full output into a bug report.
For the common "Hermes isn't at the default path" case (systemd services, Docker), **Test Connection** in the Add Server sheet now probes `/var/lib/hermes/.hermes`, `/opt/hermes/.hermes`, `/home/hermes/.hermes`, and `/root/.hermes` when it can't find `state.db` at `~/.hermes/`, and offers a one-click fill if it finds any of them.
## Features
Scarf mirrors Hermes's surface area through a sidebar-based UI. Sections below map 1:1 to the app's sidebar.
### Monitor
- **Dashboard** — System health, token usage, cost tracking, recent sessions with live refresh
- **Insights** — Usage analytics with token breakdown (including reasoning tokens), cost tracking, model/platform stats, top tools bar chart, activity heatmaps, notable sessions, and time period filtering (7/30/90 days or all time)
- **Sessions Browser** — Full conversation history with message rendering, model reasoning/thinking display, tool call inspection, full-text search, rename, delete, and JSONL export. Subagent sessions are filtered from the main list and accessible via parent session drill-down
- **Activity Feed** — Recent tool execution log with filtering by kind and session, detail inspector with pretty-printed arguments and tool output display
### Interact
- **Live Chat** — Two modes: **Rich Chat** streams responses in real-time via the Agent Client Protocol (ACP) with iMessage-style bubbles, markdown rendering, tool call visualization, thinking/reasoning display, permission request dialogs, and a one-click `/compress` focus sheet (when Hermes advertises the command); **Terminal** runs `hermes chat` with full ANSI color and Rich formatting via [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm). Both modes support session persistence, resume/continue previous sessions, auto-reconnection with session recovery, and voice mode controls
- **Memory Viewer/Editor** — View and edit Hermes's MEMORY.md and USER.md with live file-watcher refresh, external memory provider awareness (Honcho, Supermemory, etc.), and profile-scoped memory support with profile picker
- **Skills Browser** — Browse installed skills by category with file content viewer and required config warnings. **New in 1.6:** Browse the Skills Hub, search by registry (official, skills.sh, well-known, GitHub, ClawHub, LobeHub), install, check for updates, and uninstall — all from the app
### Configure *(new in 1.6)*
- **Platforms** — Native GUI setup for all 13 messaging platforms (Telegram, Discord, Slack, WhatsApp, Signal, Email, Matrix, Mattermost, Feishu, iMessage, Home Assistant, Webhook, CLI). Per-platform forms write credentials to `~/.hermes/.env` and behavior toggles to `~/.hermes/config.yaml`. WhatsApp and Signal pairing use an inline SwiftTerm terminal for QR scan and signal-cli daemon management
- **Personalities** — List defined personalities, pick the active one, and edit `SOUL.md` inline with markdown preview
- **Quick Commands** — Editor for custom `/command_name` shell shortcuts with dangerous-pattern detection (`rm -rf`, `mkfs`, etc.)
- **Credential Pools** — Per-provider credential rotation with a fixed OAuth flow (URL extraction + browser open + code paste) and proper `--type api-key` handling. API keys never stored in UI state — only last-4 preview. Strategy picker (fill_first / round_robin / least_used / random)
- **Plugins** — Install via Git URL or `owner/repo`, update, remove, enable/disable. Reads `~/.hermes/plugins/` directly for reliable state
- **Webhooks** — Create, list, test-fire, and remove webhook subscriptions. Detects the "platform not enabled" state and links to gateway setup
- **Profiles** — Switch between multiple isolated Hermes instances. Create, rename, delete, export (zip), import. Safe-switch warning reminds users to restart Scarf after activating a different profile
### Manage
- **Tools** — Enable/disable toolsets per platform with a connectivity-aware platform menu (green/orange/grey/red dots for connected/configured/offline/error). **Fixed in 1.6:** all 13 platforms now appear (was previously stuck on CLI)
- **MCP Servers** — Manage Model Context Protocol servers Hermes connects to. Add via curated presets (GitHub, Linear, Notion, Sentry, Stripe, and more) or fully custom (stdio command + args, or HTTP URL with optional bearer auth). Per-server detail view with enable/disable toggle, environment variable + header editor, tool-include/exclude filters, resources/prompts toggles, request and connect timeouts, OAuth token detection + clearing, and one-click "Test Connection" that runs `hermes mcp test` and surfaces the discovered tool list. Gateway-restart banner appears after config changes that require a reload
- **Gateway Control** — Start/stop/restart the messaging gateway, view platform connection status, manage user pairing (approve/revoke)
- **Cron Manager** — View scheduled jobs with pre-run scripts, delivery failure tracking, timeout info, and `[SILENT]` job indicators. **New in 1.6:** full write support — create, edit, pause, resume, run-now, and delete jobs from the app
- **Health** — Component-level status and diagnostics. **New in 1.6:** inline "Run Dump" and "Share Debug Report" buttons (the latter with an upload-confirmation dialog before sending to Nous support)
- **Log Viewer** — Real-time log tailing for agent.log, errors.log, and gateway.log with level filtering, component filter (Gateway / Agent / Tools / CLI / Cron), clickable session-ID pills that filter to a single session, and text search
- **Settings** — **Restructured in 1.6** into a 10-tab layout: General, Display, Agent, Terminal, Browser, Voice, Memory, Aux Models, Security, Advanced. Exposes ~60 previously hidden config fields including all 8 auxiliary model tasks, container limits, full TTS/STT provider settings, human-delay simulation, compression thresholds, logging rotation, checkpoints, website blocklist, Tirith sandbox, and delegation. One-click **Backup & Restore** via `hermes backup` / `hermes import`. Model picker replaces the old free-text model field, backed by the models.dev cache (111 providers, all major models) with a "Custom…" escape hatch
### Project Dashboards
Custom, agent-generated dashboards for any project. Define stat boxes, charts, tables, progress bars, checklists, rich text, and embedded web views in a simple JSON file — Scarf renders them with live refresh. Let your Hermes agent build and maintain project-specific visualizations automatically. See [Project Dashboards](#project-dashboards-1) below for the full schema.
### System
- **Hermes Process Control** — Start, stop, and restart the Hermes agent directly from Scarf
- **Menu Bar** — Status icon showing Hermes running state with quick actions
## Requirements
- macOS 14.6+ (Sonoma)
- Xcode 16.0+
- [Hermes agent](https://github.com/hermes-ai/hermes-agent) v0.6.0+ installed at `~/.hermes/` on each target host (v0.9.0+ recommended for full feature support)
- For remote servers: SSH access (key-based), `sqlite3` on the remote (for atomic DB snapshots), and the `hermes` CLI resolvable from the remote user's `PATH` or at a path you specify per server.
### Compatibility
Scarf reads Hermes's SQLite database and parses CLI output from `hermes status`, `hermes doctor`, `hermes tools`, `hermes sessions`, `hermes gateway`, and `hermes pairing`. Automatic schema detection provides backward compatibility with older databases while supporting new features in newer Hermes versions.
| Hermes Version | 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) |
Scarf 2.0 targets Hermes v0.10.0 for the ACP session/fork/list/resume capabilities used by remote chat. Earlier Hermes versions remain supported for monitoring, sessions, and file-based features; ACP-specific behavior may gracefully degrade on older agents.
If a Hermes update changes the database schema or CLI output format, Scarf may need to be updated. Check the [Health](#features) view for compatibility warnings.
## Install
### Pre-built Binary (no Xcode required)
Download the latest build from [Releases](https://github.com/awizemann/scarf/releases):
- `Scarf-vX.X.X-Universal.zip` — Apple Silicon + Intel (recommended)
- `Scarf-vX.X.X-ARM64.zip` — Apple Silicon only (smaller download)
1. Unzip and drag **Scarf.app** to Applications
2. Launch normally — builds are Developer ID signed and notarized, so Gatekeeper accepts them on first launch
Scarf checks for updates automatically on launch via [Sparkle](https://sparkle-project.org) and daily thereafter. You can disable automatic checks or trigger a manual check from **Settings → General → Updates** or the menu bar icon.
### Build from Source
```bash
git clone https://github.com/awizemann/scarf.git
cd scarf/scarf
open scarf.xcodeproj
```
Or from the command line:
```bash
xcodebuild -project scarf/scarf.xcodeproj -scheme scarf -configuration Release -arch arm64 -arch x86_64 ONLY_ACTIVE_ARCH=NO build
```
## Architecture
Scarf follows the **MVVM-Feature** pattern with zero external dependencies beyond SwiftTerm:
```
scarf/
Core/
Models/ Plain data structs (HermesSession, HermesMessage, HermesConfig, etc.)
Services/ Data access (SQLite reader, file I/O, log tailing, file watcher)
Features/ Self-contained feature modules
Dashboard/ System overview and stats
Insights/ Usage analytics and activity patterns
Sessions/ Conversation browser with rename, delete, export
Activity/ Tool execution feed with inspector
Projects/ Agent-generated project dashboards with widget rendering
Chat/ Rich ACP chat and embedded terminal with voice controls
Memory/ Memory viewer and editor
Skills/ Skill browser by category
Tools/ Toolset management per platform
MCPServers/ MCP server registry, presets, OAuth, tool filters, test runner
Gateway/ Messaging gateway control and pairing
Cron/ Scheduled job viewer
Logs/ Real-time log viewer
Settings/ Structured config editor
Navigation/ AppCoordinator + SidebarView
```
### Data Sources
Scarf reads Hermes data directly from `~/.hermes/`:
| Source | Format | Access |
|--------|--------|--------|
| `state.db` | SQLite (WAL mode) | Read-only |
| `config.yaml` | YAML | Read-only |
| `memories/*.md` | Markdown | Read/Write |
| `cron/jobs.json` | JSON | Read-only |
| `logs/*.log` | Text | Read-only |
| `gateway_state.json` | JSON | Read-only |
| `skills/` | Directory tree | Read-only |
| `hermes acp` | ACP subprocess (JSON-RPC stdio) | Real-time chat |
| `hermes chat` | Terminal subprocess | Interactive |
| `hermes tools` | CLI commands | Enable/Disable |
| `hermes sessions` | CLI commands | Rename/Delete/Export |
| `hermes gateway` | CLI commands | Start/Stop/Restart |
| `hermes pairing` | CLI commands | Approve/Revoke |
| `hermes mcp` | CLI commands | Add/Remove/Test MCP servers |
| `mcp-tokens/*.json` | JSON (per-server OAuth) | Detect/Delete |
| `.scarf/dashboard.json` | JSON (per-project) | Read-only |
| `scarf/projects.json` | JSON (registry) | Read/Write |
The app opens `state.db` in read-only mode to avoid WAL contention with Hermes. Management actions (tool toggles, session rename/delete/export) go through the Hermes CLI.
### Dependencies
| Package | Purpose |
|---------|---------|
| [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm) | Terminal emulator for the Chat feature |
| [Sparkle](https://github.com/sparkle-project/Sparkle) | Auto-updates from the GitHub-hosted appcast |
Everything else uses system frameworks: SQLite3 C API, Foundation JSON, AttributedString markdown, SwiftUI Charts, GCD file watching.
## How It Works
Scarf watches `~/.hermes/` for file changes and queries the SQLite database for sessions, messages, and analytics. Views refresh automatically when Hermes writes new data.
The Chat tab has two modes. **Rich Chat** communicates with Hermes via the Agent Client Protocol (ACP) — a JSON-RPC connection over stdio — streaming responses in real-time with automatic reconnection and session recovery on connection loss. **Terminal** mode spawns `hermes chat` in a pseudo-terminal for the full interactive CLI experience with proper ANSI rendering. Sessions persist across navigation in both modes — switch tabs and come back without losing your conversation.
Management actions (renaming sessions, toggling tools, editing memory) call the Hermes CLI or write directly to the appropriate files, keeping Scarf and Hermes in sync.
The app sandbox is disabled because Scarf needs direct access to `~/.hermes/` and the ability to spawn the Hermes binary.
## Project Dashboards
Project Dashboards turn Scarf into a customizable monitoring hub for all your projects. You define a simple JSON file in your project folder describing what to display — stat boxes, charts, tables, progress bars, checklists, rich text, and embedded web views — and Scarf renders it as a live-updating dashboard. Your Hermes agent can generate and maintain these dashboards automatically.
### What You Can Build
- **Development dashboards** — test coverage, build status, open issues, sprint progress
- **Data project trackers** — pipeline metrics, data quality scores, processing throughput
- **Deployment monitors** — deploy history tables, uptime stats, error rate charts
- **Research dashboards** — experiment results, key findings, paper status checklists
- **Agent activity views** — cron job results, content generation stats, task completion rates
- **Embedded web apps** — local dev servers, HTML reports, Grafana dashboards, any web-based tool your agent generates
- **Any project status** — if your agent can measure it, Scarf can display it
### Quick Start
**1. Create the dashboard file**
Create `.scarf/dashboard.json` in any project folder:
```json
{
"version": 1,
"title": "My Project",
"description": "Project status at a glance",
"sections": [
{
"title": "Overview",
"columns": 3,
"widgets": [
{
"type": "stat",
"title": "Test Coverage",
"value": "87%",
"icon": "checkmark.shield",
"color": "green",
"subtitle": "+2.1% this week"
},
{
"type": "progress",
"title": "Sprint Progress",
"value": 0.73,
"label": "73% complete",
"color": "blue"
},
{
"type": "list",
"title": "Tasks",
"items": [
{ "text": "Write unit tests", "status": "done" },
{ "text": "Update API docs", "status": "active" },
{ "text": "Deploy to prod", "status": "pending" }
]
}
]
}
]
}
```
**2. Register your project**
In Scarf, go to **Projects** in the sidebar and click the **+** button to add your project folder. Or have your agent add it directly to the registry at `~/.hermes/scarf/projects.json`:
```json
{
"projects": [
{ "name": "my-project", "path": "/Users/you/Developer/my-project" }
]
}
```
**3. View in Scarf**
Select your project in the Projects sidebar — the dashboard renders immediately. Scarf watches the file for changes and refreshes automatically whenever the JSON is updated.
### Widget Types
| Type | Description | Key Fields |
|------|-------------|------------|
| `stat` | Key metric with large value display | `value`, `icon`, `color`, `subtitle` |
| `progress` | Progress bar with label | `value` (0.01.0), `label`, `color` |
| `text` | Rich text block | `content`, `format` ("markdown" or "plain") |
| `table` | Data table with headers | `columns`, `rows` |
| `chart` | Line, bar, or pie chart | `chartType`, `series` (each with `name`, `color`, `data`) |
| `list` | Checklist with status indicators | `items` (each with `text`, `status`: done/active/pending) |
| `webview` | Embedded web browser | `url`, `height` (default 400) |
The `webview` widget embeds a live web browser directly in your dashboard — perfect for displaying local dev servers, HTML reports, or any web-based tool your agent generates.
When a dashboard includes a webview widget, Scarf adds a tabbed interface: **Dashboard** shows your normal widgets, **Site** shows the web content full-canvas with clean margins — using the entire available space in the app. This gives you the best of both worlds: compact metrics at a glance, and a full embedded browser when you need it.
```json
{
"type": "webview",
"title": "Project Report",
"url": "http://localhost:8000/dashboard",
"height": 500
}
```
- `url`: Any URL — typically a local server (`http://localhost:...`) or file path
- `height`: Height in points when displayed as an inline widget card (default: 400). The Site tab always uses full available space regardless of this setting.
**Colors**: red, orange, yellow, green, blue, purple, pink, teal, indigo, mint, brown, gray
**Icons**: Any [SF Symbol](https://developer.apple.com/sf-symbols/) name (e.g., `checkmark.shield`, `cpu`, `doc.text`, `chart.bar`)
### Agent-Generated Dashboards
The real power is letting your Hermes agent build and update dashboards automatically. Add instructions like this to your agent's context:
> Analyze this project and create a `.scarf/dashboard.json` dashboard with relevant metrics and status. Use stat widgets for key numbers, charts for trends, tables for structured data, lists for task tracking, and a webview widget if the project has a local web server or HTML reports. Register the project in `~/.hermes/scarf/projects.json` if not already registered.
Your agent can update the dashboard as part of cron jobs, after builds, or whenever project state changes. Since Scarf watches the file, updates appear in real-time.
### Dashboard Schema Reference
```json
{
"version": 1,
"title": "Required — dashboard title",
"description": "Optional — subtitle text",
"updatedAt": "Optional — ISO 8601 timestamp",
"sections": [
{
"title": "Section Name",
"columns": 3,
"widgets": [{ "type": "...", "title": "..." }]
}
]
}
```
Each section defines a grid with 14 columns. Widgets flow left-to-right, wrapping to new rows. See [DASHBOARD_SCHEMA.md](scarf/docs/DASHBOARD_SCHEMA.md) for the full schema reference with examples of every widget type.
## Releases
Scarf ships through GitHub releases — the App Store is not supported because Scarf spawns the user-installed `hermes` binary and reads `~/.hermes/` directly, both of which App Sandbox forbids.
Each release goes through a single local script: [scripts/release.sh](scripts/release.sh). The script archives a universal binary, signs it with the Developer ID Application cert, submits to `notarytool`, staples the ticket, produces the distribution zip, signs an appcast entry with Sparkle's EdDSA key, pushes an updated `appcast.xml` to the `gh-pages` branch, creates the GitHub release, and tags `main`.
The Sparkle appcast is served from [awizemann.github.io/scarf/appcast.xml](https://awizemann.github.io/scarf/appcast.xml).
Signing prerequisites (one-time):
- `Developer ID Application` certificate in the login Keychain
- `scarf-notary` keychain profile registered via `xcrun notarytool store-credentials`
- Sparkle EdDSA private key in Keychain item `https://sparkle-project.org` (back this up — without it, shipped apps can never receive updates)
## Template Catalog
Community-contributed Scarf project templates live under [`templates/`](templates/) in this repo and are browsable at **[awizemann.github.io/scarf/templates/](https://awizemann.github.io/scarf/templates/)** with live dashboard previews and one-click `scarf://install?url=…` links.
- **Install from the web** — click "Install with Scarf" on any template's detail page; the app takes over from there.
- **Install from a local file** — Scarf → Projects → Templates → Install from File…, or double-click any `.scarftemplate` in Finder.
- **Author a template** — see [`templates/CONTRIBUTING.md`](templates/CONTRIBUTING.md) for the full walkthrough. Fork, drop a template under `templates/<your-github-handle>/<your-name>/`, open a PR; CI validates the bundle automatically.
The catalog's site is a static HTML + vanilla JS build generated by [`tools/build-catalog.py`](tools/build-catalog.py) and driven by [`scripts/catalog.sh`](scripts/catalog.sh) (check / build / preview / publish). Appcast and main landing page are independent — updating the catalog never disturbs Sparkle.
## Contributing
Contributions are welcome. Please open an issue to discuss what you'd like to change before submitting a PR.
1. Fork the repo
2. Create your feature branch (`git checkout -b feature/my-feature`)
3. Commit your changes (`git commit -m 'Add my feature'`)
4. Push to the branch (`git push origin feature/my-feature`)
5. Open a Pull Request
For template submissions, see [`templates/CONTRIBUTING.md`](templates/CONTRIBUTING.md) — same flow, with a catalog-specific checklist + automated CI validation.
## Support
If you find Scarf useful, consider buying me a coffee.
<a href="https://www.buymeacoffee.com/awizemann"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" height="40"></a>
## License
[MIT](LICENSE)
-106
View File
@@ -1,106 +0,0 @@
// Scarf landing page — minimal client behavior.
// No dependencies. Runs after defer-parse.
(function () {
const root = document.documentElement;
const STORAGE_KEY = 'scarf-theme';
function applyTheme(theme) {
if (theme === 'light' || theme === 'dark') {
root.setAttribute('data-theme', theme);
} else {
root.removeAttribute('data-theme');
}
applyImageTheme();
}
// Resolve the *effective* theme — explicit data-theme wins, otherwise
// fall back to the OS preference.
function resolveTheme() {
const explicit = root.getAttribute('data-theme');
if (explicit === 'light' || explicit === 'dark') return explicit;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
// Swap every <img data-dark-src="..."> between its light and dark variants.
// Also rewrites the parent <picture>'s <source srcset> so the picture
// algorithm doesn't override us on resize/layout passes.
function applyImageTheme() {
const theme = resolveTheme();
document.querySelectorAll('img[data-dark-src]').forEach((img) => {
if (!img.dataset.lightSrc) {
img.dataset.lightSrc = img.getAttribute('src');
}
const target = theme === 'dark' ? img.dataset.darkSrc : img.dataset.lightSrc;
if (img.getAttribute('src') !== target) img.setAttribute('src', target);
const picture = img.parentElement;
if (picture && picture.tagName === 'PICTURE') {
picture.querySelectorAll('source').forEach((s) => {
if (s.getAttribute('srcset') !== target) s.setAttribute('srcset', target);
});
}
});
}
// Hydrate stored preference (if any) — runs after DOMContentLoaded since
// the <script> is deferred. There's a brief moment of media-query default
// before hydrate; that's acceptable here (no FOUC because the media query
// already gets the right colors and the first images render at light by
// default — JS swaps within a frame on dark-mode systems).
let stored = null;
try {
stored = localStorage.getItem(STORAGE_KEY);
if (stored === 'light' || stored === 'dark') applyTheme(stored);
else applyImageTheme(); // initial pass even if no stored preference
} catch (_) {
applyImageTheme();
}
const toggle = document.querySelector('[data-theme-toggle]');
if (toggle) {
toggle.addEventListener('click', () => {
const current = root.getAttribute('data-theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
let next;
if (current === 'light') next = 'dark';
else if (current === 'dark') next = null;
else next = prefersDark ? 'light' : 'dark';
applyTheme(next);
try {
if (next) localStorage.setItem(STORAGE_KEY, next);
else localStorage.removeItem(STORAGE_KEY);
} catch (_) { /* ignore */ }
});
}
// Re-apply on system preference change so users who haven't set an
// explicit override still get matching screenshots.
if (window.matchMedia) {
const mql = window.matchMedia('(prefers-color-scheme: dark)');
const onChange = () => {
if (!root.hasAttribute('data-theme')) applyImageTheme();
};
if (mql.addEventListener) mql.addEventListener('change', onChange);
else if (mql.addListener) mql.addListener(onChange);
}
// Auto-collapse sticky header on scroll-down, restore on scroll-up.
const header = document.querySelector('.site-header');
if (header) {
let lastY = window.scrollY;
let ticking = false;
window.addEventListener('scroll', () => {
if (ticking) return;
window.requestAnimationFrame(() => {
const y = window.scrollY;
if (y > 80 && y > lastY) header.style.transform = 'translateY(-100%)';
else header.style.transform = '';
lastY = y;
ticking = false;
});
ticking = true;
}, { passive: true });
header.style.transition = 'transform 0.25s ease';
}
})();
-785
View File
@@ -1,785 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Scarf Updates</title>
<link>https://awizemann.github.io/scarf/appcast.xml</link>
<description>Scarf macOS app updates</description>
<language>en</language>
<item>
<title>Version 2.8.0</title>
<sparkle:version>35</sparkle:version>
<sparkle:shortVersionString>2.8.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sat, 09 May 2026 19:02:51 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.8.0</h2>
<p>A coordinated catch-up release adopting Hermes v0.13.0 (v2026.5.7) — &quot;The Tenacity Release&quot; — across Scarf&#x27;s full surface area. v2.8.0 ships <strong>Persistent Goals</strong>, <strong>ACP <code>/queue</code></strong>, <strong>Kanban diagnostics + recovery UX</strong>, <strong>Curator archive/prune</strong>, <strong>Google Chat (20th platform) + cross-platform allowlists</strong>, a refreshed <strong>provider catalog</strong> with five new models, and a slate of settings + UX polish — all behind capability flags so pre-v0.13 hosts continue to render the v2.7.5 surface unchanged.</p>
<p>No data migrations, no schema changes. <code>~/.hermes/state.db</code> columns are unchanged from v0.11/v0.12. Existing <code>~/.hermes/scarf/</code> sidecars are untouched. Sparkle picks the update up automatically.</p>
<h3>New features</h3>
<h4>Persistent Goals + ACP <code>/queue</code> (chat)</h4>
<ul>
<li><strong><code>/goal &lt;text&gt;</code> slash command</strong> — locks the agent on a target that persists across turns. Surfaced via the chat slash menu (gated on <code>HermesCapabilities.hasGoals</code>) and rendered as an <code>info</code>-tinted &quot;Goal locked: …&quot; pill in the chat header. The pill exposes a &quot;Clear goal&quot; context-menu item that dispatches <code>/goal --clear</code>. Optimistic local mirror — Hermes is the authoritative owner; Scarf paints the pill the moment the user sends <code>/goal …</code> so the affordance feels instant.</li>
<li><strong><code>/queue &lt;text&gt;</code> slash command</strong> — queues a prompt to run after the current turn completes. Joins <code>/steer</code> and <code>/goal</code> in <code>RichChatViewModel.nonInterruptiveCommands</code> (the chat keeps &quot;Agent working…&quot; off when sent). A header chip shows the queued count; tap opens a popover listing prompts + relative timestamps. Per-entry deletion isn&#x27;t exposed (Hermes has no remove-by-id verb), and the popover header makes that explicit so users understand the local mirror&#x27;s role.</li>
<li><strong><code>/steer</code> on idle</strong> — pre-v0.13 was a no-op when no turn was in flight; v0.13 runs it as a regular prompt. The composer&#x27;s slash button now greys <code>/steer</code> only on pre-v0.13 hosts (gated on <code>hasACPSteerOnIdle</code>).</li>
<li><strong>Static slash-menu fallbacks</strong> — pre-session, the menu surfaces <code>/new</code> (with optional <code>[&lt;name&gt;]</code> argument hint on v0.13). Active-session-only fallbacks (<code>/clear</code>, <code>/compact</code>, <code>/cost</code>, <code>/model</code>, <code>/tools</code>, <code>/reload-skills</code>, <code>/help</code>, <code>/exit</code>) round out resumed sessions where Hermes ACP doesn&#x27;t re-emit <code>available_commands_update</code> after <code>session/load</code>. Deduped against the ACP-advertised set so the canonical entry always wins once a session opens.</li>
</ul>
<h4>Kanban v0.13 diagnostics + recovery UX</h4>
<ul>
<li><strong>Hallucination-gate verify / reject</strong> — worker-created cards land with <code>hallucination_gate_status: pending</code>. The inspector renders a yellow banner (&quot;Created by a worker — verify before running&quot;) with a Verify and Reject button. Cards in pending state dim 0.6 with a yellow ⚠ glyph in the title row.</li>
<li><strong>Diagnostics rendering</strong> — new typed-mirror enum <code>KanbanDiagnosticKind</code> with severity (info / warning / critical). Per-task and per-run diagnostics surface in the inspector Runs tab as chip-lists; auto-block reasons render verbatim in the existing red banner. Darwin zombie detections show as a distinct <code>darwin_zombie_detected</code> kind.</li>
<li><strong>Per-task <code>max_retries</code></strong> — added to the create sheet (default 3) and shown as a header chip in the inspector. Write-once at create time, matching Hermes&#x27;s pattern.</li>
<li><strong>Multiline title/body</strong> — the create sheet&#x27;s Title field accepts multiline input, capped to four visible rows.</li>
<li><strong>Tolerant decoding</strong> — every new field uses <code>decodeIfPresent</code>. Pre-v0.13 JSON parses cleanly with the new fields defaulting to nil, and the v2.7.5 board surface is unchanged on older hosts.</li>
</ul>
<h4>Curator archive + prune</h4>
<ul>
<li><strong>Archived skills section</strong> in <code>CuratorView</code> showing <code>hermes curator list-archived</code> output. Each row exposes Restore (returns to the active leaderboard) and Prune (destructive — opens a custom confirm sheet matching the template-uninstall pattern, with <code>ScarfDestructiveButton</code> &quot;Prune permanently&quot; and Cancel as the default keyboard action).</li>
<li><strong>Bulk prune</strong> — a header action (gated on archived list non-empty) that enumerates every archived skill in the confirm sheet before a single-tap destructive action. Per-skill prune buttons are present per row when Hermes supports <code>prune &lt;name&gt;</code>; otherwise only the bulk action is exposed.</li>
<li><strong>Synchronous &quot;Run Now&quot;</strong> — v0.13 <code>hermes curator run</code> blocks until done. The Run Now button shows a progress affordance for the duration; pre-v0.13 falls back to fire-and-forget.</li>
<li><strong>New <code>CuratorService</code> actor</strong> in ScarfCore (<a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Services/CuratorService.swift">scarf/Packages/ScarfCore/Sources/ScarfCore/Services/CuratorService.swift</a>) — pure-I/O Sendable actor mirroring <code>KanbanService</code>&#x27;s shape, with defensive <code>--json</code> retry-without-flag fallback for verbs that may not support it on all v0.13 patch releases.</li>
<li>The legacy <code>CuratorRestoreSheet</code> flow (SAFE-list-restore for v0.12) is preserved; it predates the v0.13 archive surface and serves a distinct case.</li>
</ul>
<h4>Messaging Gateway expansion</h4>
<ul>
<li><strong>Google Chat</strong> — 20th platform. New entry in the Mac Platforms tab, gated on <code>HermesCapabilities.hasGoogleChatPlatform</code>.</li>
<li><strong>Cross-platform allowlists</strong> — per-platform editor for <code>allowed_channels</code> (Slack / Mattermost / Google Chat), <code>allowed_chats</code> (Telegram / WhatsApp), and <code>allowed_rooms</code> (Matrix / DingTalk). New <code>AllowlistEditor</code> component plus the <code>GatewayAllowlistKind</code> / <code>GatewayPlatformSettings</code> ScarfCore types. Persisted to <code>~/.hermes/config.yaml</code> via a new <code>GatewayConfigWriter</code> since <code>hermes config set</code> doesn&#x27;t write list blocks.</li>
<li><strong>Per-platform behavior toggles</strong> — <code>busy_ack_enabled</code> (suppress per-message &quot;agent is working…&quot; acks), <code>gateway_restart_notification</code> (post a &quot;Gateway restarted&quot; notice on boot), and a slash-command auto-delete TTL (seconds, 0 to disable). Each appears in the new <code>GatewayBehaviorSection</code> component.</li>
<li><strong><code>hermes gateway list</code> cross-profile digest</strong> — inline status row in <code>MessagingGatewayView</code> showing which profile is running which platform across all profiles. New <code>HermesGatewayListService</code> actor parses <code>hermes gateway list --json</code>. Hidden when the verb fails (pre-v0.13 hosts) or no profiles are registered.</li>
<li><strong><code>MessagingGatewayViewModel</code></strong> — internal rename from <code>GatewayViewModel</code> to disambiguate from the v0.10 Tool Gateway feature. The user-facing label was already &quot;Messaging Gateway&quot; since v0.10.</li>
<li><strong><code>[[as_document]]</code> hint</strong> — informational tooltip in skill detail surfaces explaining the new media-routing directive for skills that reference it.</li>
</ul>
<h4>Provider catalog refresh</h4>
<ul>
<li><strong>Five new models</strong> — <code>deepseek/deepseek-v4-pro</code>, <code>x-ai/grok-4.3</code>, <code>openrouter/owl-alpha</code> (free tier), <code>tencent/hy3-preview</code>, and <code>arcee/trinity-large-thinking</code> (with temperature + compression overrides). Surfaced through <code>models_dev_cache.json</code>; no manual entries required.</li>
<li><strong>Grok rename</strong> — <code>x-ai/grok-4.20-beta</code> → <code>x-ai/grok-4.20</code>. Implemented via read-time alias resolution in <code>ModelCatalogService.modelAliases</code> so existing user configs with the <code>-beta</code> suffix keep validating without YAML rewrites. Three composite-keyed aliases cover the openrouter / xai / vercel routes.</li>
<li><strong>Vercel AI Gateway demoted</strong> — sort comparator change in <code>loadProviders()</code> puts Vercel last, after the alphabetical group.</li>
<li><strong><code>image_gen.model</code> honored</strong> — pre-v0.13 the key was advertised but ignored; v0.13 actually drives the image-generation path. Surfaced in <code>Settings → Auxiliary</code> with a curated picker (<code>OpenAI gpt-image-1</code>, <code>Imagen 3/4</code>, <code>Stable Image Ultra</code>, <code>FLUX 1.1 Pro</code>, <code>DALL·E 3</code>); free-form entry is also accepted. Gated on <code>hasImageGenModel</code>.</li>
<li><strong>OpenRouter response caching</strong> — toggle in <code>Settings → Auxiliary</code> writing <code>openrouter.response_cache.enabled</code> to <code>config.yaml</code>. Off by default in Scarf&#x27;s parser. Gated on <code>hasOpenRouterResponseCache</code>.</li>
</ul>
<h4>Settings tab additions</h4>
<ul>
<li><strong>MCP SSE transport</strong> — MCP add-server flow gains a Transport picker (<code>stdio</code> / <code>http</code> / <code>sse</code>) with <code>sse_read_timeout</code> field for SSE servers. The YAML round-trip preserves OAuth + headers identically to the existing <code>.http</code> shape. Gated on <code>hasMCPSSETransport</code>.</li>
<li><strong>Cron <code>--no-agent</code> watchdog mode</strong> — toggle in the Cron edit sheet that maps to <code>hermes cron create/update --no-agent</code>. When ON, the prompt + context fields hide (the AI call is skipped). Defensive write-path strips the flag on pre-v0.13 hosts mirroring the <code>--workdir</code> pattern. New <code>HermesCronJob.noAgent: Bool</code> field with <code>decodeIfPresent</code> so pre-v0.13 reads keep parsing. Gated on <code>hasCronNoAgent</code>.</li>
<li><strong>Web Tools per-capability backends</strong> — new <code>Settings → Web Tools</code> tab with separate pickers for <code>web_search</code> and <code>web_extract</code>. SearXNG appears in the search picker only. The legacy single <code>web_tools.backend</code> is still readable for round-trip safety on mixed-version installs. Gated on <code>hasWebToolsBackendSplit</code>.</li>
<li><strong>Profiles <code>--no-skills</code></strong> — &quot;Empty profile (no skills)&quot; toggle in the create-profile flow that appends <code>--no-skills</code> to <code>hermes profile create</code>. Disabled when &quot;Clone all&quot; is on (mutually exclusive). Gated on <code>hasProfileNoSkills</code>.</li>
</ul>
<h4>UX polish</h4>
<ul>
<li><strong>Context compression count</strong> in the chat status bar. v0.13 emits the count alongside the token tally on the <code>session/prompt</code> response; Scarf renders a <code>🗜 ×N</code> chip next to the token count when <code>count &gt; 0</code>. Gated on <code>hasContextCompressionCount</code>.</li>
<li><strong><code>/new &lt;name&gt;</code> argument hint</strong> — bracket-aware so v0.13 hosts show <code>[&lt;name&gt;]</code> and pre-v0.13 hosts show no hint.</li>
<li><strong><code>HermesUpdaterCommandBuilder</code></strong> — forward-compat plumbing for <code>hermes update --yes</code>. No in-app surface in v2.8.0 (Scarf doesn&#x27;t currently expose a &quot;Run hermes update&quot; command); the builder is wired so a future Settings affordance can opt in cleanly.</li>
<li><strong>Redaction default-flip awareness</strong> — the existing <code>Settings → Advanced → Redaction</code> toggle hint copy now branches on <code>HermesCapabilities.isV013OrLater</code>. v0.13+ hosts read &quot;Recommended: ON. Hermes v0.13 defaults to redacting secrets unless you opt out&quot;; pre-v0.13 keeps the v2.7 hint.</li>
<li><strong><code>display.language</code> picker</strong> — new <code>Settings → General → Locale</code> row. 8 options: default, zh, ja, de, es, fr, uk, tr. Hermes does the actual translation; Scarf just persists <code>display.language</code> to <code>config.yaml</code>. Gated on <code>hasDisplayLanguage</code>.</li>
<li><strong>xAI Custom Voices badge</strong> — <code>Settings → Voice</code> shows a &quot;Cloning supported&quot; <code>ScarfBadge</code> next to the xAI TTS provider entry. Informational only; voice management itself happens via <code>hermes voice</code> CLI. Gated on <code>hasXAIVoiceCloning</code>.</li>
</ul>
<h4>ScarfGo iOS catch-up (read-only)</h4>
<p>Following the Phase H precedent, iOS mirrors selected v2.8 surfaces as read-only — write parity is deferred to v2.8.x.</p>
<ul>
<li><strong>Goal pill + queue chip</strong> in the iOS chat header (<code>projectContextBar</code>). Tap is a no-op; the Mac app owns mutations.</li>
<li><strong>Kanban v0.13 diagnostics</strong> in <code>ScarfGoKanbanDetailSheet</code> — <code>retries: N</code> chip, &quot;Worker-created — verify on Mac&quot; hallucination badge, red <code>auto_blocked_reason</code> banner, tappable diagnostics chip-lists with severity-tinted badges and a new <code>DiagnosticDetailSheet</code> (replacing Mac&#x27;s <code>.help()</code> tooltip on touch).</li>
<li><strong>Curator Archived list</strong> in <code>Scarf iOS/Curator/CuratorView.swift</code> — read-only, with footer pointing users to the Mac app for Restore / Prune actions.</li>
<li><strong>Settings → Platforms extension</strong> — Google Chat status row, busy-ack and restart-notification summary rows across <code>gatewayPlatforms</code> (handles disagreement with &quot;mixed (N platforms)&quot;), allowlist DisclosureGroups with monospaced &quot;platform: id&quot; entries when expanded.</li>
<li><strong>&quot;v0.13 features active&quot; badge</strong> in iOS Settings (gated on <code>caps.isV013OrLater</code>). Tap presents <code>V013FeaturesSheet</code> listing the new affordances.</li>
</ul>
<h3>Capability gating</h3>
<p>v2.8.0 adds 22 new flags on <code>HermesCapabilities</code> (each gating one v0.13 surface), plus an <code>isV013OrLater</code> convenience predicate. Every new affordance is gated; pre-v0.13 hosts see the v2.7.5 surface byte-identical to before. The HermesVersionBanner threshold remains pre-v0.12 — v0.12 → v0.13 nudging happens via the iOS Settings badge (positive surface) rather than a global yellow banner (which was reserved for &quot;missing every new feature&quot; cases).</p>
<h3>Bug fixes uncovered during v0.13.0 dogfooding</h3>
<ul>
<li><strong>Dashboard flicker on v0.13 hosts</strong> — Hermes v0.13 writes to <code>state.db-wal</code> and rotating logs at ~10 Hz during gateway activity. Each FSEvents fire ticked <code>lastChangeDate</code>, every observing view re-fired its load handler against it, and on Local hosts the dashboard stacked 5+ concurrent <code>dashboardSnapshot</code> calls in 200 ms — sqlite contention on the read-only handle surfaced as <code>BackendError error 3</code>, plus visible flicker. Two-part fix: <code>HermesFileWatcher.scheduleCoalescedTick</code> coalesces FSEvents into one observable mutation per 500 ms quiet window with a 1.5 s max-wait floor (so a coincident <code>gateway_state.json</code> Start/Stop touch can&#x27;t be starved indefinitely under sustained WAL writes); <code>DashboardViewModel.load()</code> holds a single in-flight <code>Task&lt;Void, Never&gt;</code> handle so concurrent triggers await the in-flight load instead of stacking.</li>
<li><strong>Sparse slash menu on resumed sessions</strong> — Hermes ACP only emits <code>available_commands_update</code> after <code>session/new</code>, not after <code>session/load</code>. Combined with <code>RichChatViewModel.reset()</code> clearing <code>acpCommands</code> on every session switch, resumed sessions landed at a 4-command fallback even though the agent identity hadn&#x27;t changed. Fix: stop wiping <code>acpCommands</code> in <code>reset()</code> (they&#x27;re agent-level, not session-level), and add an active-session-only static fallback set covering the standard agent commands so cold-start LOAD users see a rich menu immediately.</li>
</ul>
<h3>Migrating from 2.7.5</h3>
<p>Sparkle delivers the update automatically. No config migration, no schema changes — same <code>~/.hermes/state.db</code> columns as v0.11/v0.12, same Scarf-owned sidecars at <code>~/.hermes/scarf/</code>. Existing v2.7.5 Kanban tenants stay valid; existing project manifests are unchanged. Settings tabs grow new rows; existing rows render identically.</p>
<p>If you&#x27;re connecting to a Hermes v0.13.0 host for the first time after this update, the new surfaces light up automatically — no flag flip in the app. Pre-v0.13 hosts continue to render the v2.7.5 surface; nothing breaks if you upgrade Scarf before upgrading Hermes.</p>
<h3>Known limitations</h3>
<ul>
<li><strong>iOS write surfaces</strong> (Verify hallucination gate, Reject, Curator archive/prune actions, allowlist editor, <code>/goal</code> send, <code>/queue</code> send) are explicitly out of scope for v2.8.0 and slated for v2.8.x. iOS surfaces are read-only mirrors per the Phase H precedent.</li>
<li><strong>Auto-resumed-from-checkpoint indicator</strong> — Hermes v0.13&#x27;s &quot;auto-resume after gateway restart&quot; feature is server-side; whether the ACP adapter advertises a Scarf-visible signal is unclear pending live host verification. Deferred to v2.8.1.</li>
<li><strong>xAI voice cloning management UX</strong> — only the &quot;Cloning supported&quot; badge ships in v2.8.0. A full voice-management surface is a follow-up.</li>
<li><strong>Bulk re-tag for legacy NULL-tenant Kanban tasks</strong> — carryover from v2.7.5; Hermes still has no <code>tenant</code> mutation verb post-create.</li>
<li><strong>Cluster A wire-shape TODOs</strong> — 25 <code>// TODO(WS-N-Q&lt;n&gt;)</code> markers across the codebase flag fields and CLI flags whose exact shape couldn&#x27;t be verified from release notes alone. Each has a tolerant-decode default that fails closed (hides the affordance rather than throwing); a pre-merge sweep on a v0.13 host can confirm or fix each in seconds.</li>
</ul>
<h3>Acknowledgements</h3>
<p>v2.8.0 was driven by a 9-stream coordinated multi-agent build: WS-1 capability flag foundation through WS-9 iOS catch-up, with planning artifacts archived under <a href="scarf/docs/v2.8/">scarf/docs/v2.8/</a> for future reference. Bug fixes for the dashboard flicker and sparse-slash-menu issues were caught during a fresh end-to-end dogfood pass against a live Hermes v0.13.0 install — the kind of surface-level UX bugs that only show up under real-world <code>state.db-wal</code> write rates and real-world resume flows. As always, real bugs come from doing instead of speculating.</p>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.8.0/Scarf-v2.8.0-Universal.zip"
sparkle:edSignature="m5HQUKgxfWa5u88gEVCGWMIKaogBIsjPspQG97y1KcrW1w6S5XF1s0v1oRaRWMyIlj46BD+937Inu2Ii5TbXAg=="
length="19771149"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.7.5</title>
<sparkle:version>34</sparkle:version>
<sparkle:shortVersionString>2.7.5</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 08 May 2026 10:56:09 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.7.5</h2>
<p>A feature release that lifts Scarf&#x27;s Kanban surface from a read-only list (the v2.6 placeholder shipped while upstream Kanban was still mid-rework) to a full drag-and-drop board with the complete Hermes v0.12 mutation surface wired up — plus per-project boards bound to a Scarf-minted tenant slug, and a read-only board on iOS for at-a-glance status from your phone. No data migrations, no schema changes; pre-v0.12 hosts gracefully hide the surface.</p>
<h3>New features</h3>
<h4>Mac</h4>
<ul>
<li><strong>Drag-and-drop Kanban board</strong> (<a href="scarf/scarf/Features/Kanban/Views/KanbanBoardView.swift">scarf/Features/Kanban/Views/KanbanBoardView.swift</a>). Five visible columns — Triage / Up Next (<code>todo</code> + <code>ready</code>) / Running / Blocked / Done — collapsing Hermes&#x27;s seven status values into a layout that doesn&#x27;t waste space on <code>ready</code>, which the dispatcher only ever holds for a few seconds. Triage hides itself when empty; archived hides behind a header toggle. Drop a card onto a column and Scarf maps the gesture to the right Hermes verbs through a pure transition planner: drop-on-Running fires <code>kanban dispatch</code> (the dispatcher then spawns a worker), drop-on-Blocked opens a sheet asking for a reason and calls <code>kanban block</code>, drop-on-Done opens a result sheet and calls <code>kanban complete</code>, blocked → running chains <code>unblock</code> + <code>dispatch</code>. Forbidden transitions (anything dropped on Done; anything dragged out of Triage) reject with a red drop-target stroke and a tooltip explaining why — Done is terminal, Triage is promoted by a specifier worker, neither has a CLI verb that maps cleanly. Optimistic local updates apply on drop and revert on CLI failure with a toast, so the UI feels instant.</li>
</ul>
<ul>
<li><strong>Side-pane inspector</strong> (<a href="scarf/scarf/Features/Kanban/Views/KanbanInspectorPane.swift">KanbanInspectorPane.swift</a>). Click a card and a 420 px pane slides in from the trailing edge. Not a modal sheet — modal would block triaging the next card after closing. Header carries the status, an inline assignee menu (more on that below), workspace kind, and tenant; below that, four tabs render <code>hermes kanban show &lt;id&gt;</code> data: <strong>Comments</strong> (with an inline composer that calls <code>kanban comment</code>), <strong>Events</strong> (the <code>task_events</code> log with per-kind glyphs), <strong>Runs</strong> (one row per attempt with outcome badge + summary + error), and <strong>Log</strong> — the worker&#x27;s captured stdout/stderr from <code>hermes kanban log &lt;id&gt;</code>, polled every 2 s while the task is running with a &quot;● streaming&quot; indicator and auto-scroll to the latest line, snapshot-only with a refresh button when the task is in a terminal state. The action bar at the bottom has all the per-status verbs — Start (which is <code>claim</code> rebranded as a user-visible action), Complete, Block, Unblock, Archive — every one with a help tooltip explaining what it does and what Hermes verb it invokes. The &quot;Archive&quot; tooltip explicitly notes Hermes has no hard-delete: archived tasks remain in <code>~/.hermes/kanban.db</code> and are recoverable via the &quot;Show archived&quot; toggle until <code>hermes kanban gc</code> runs.</li>
</ul>
<ul>
<li><strong>Inspector auto-refresh.</strong> While the inspector is open, the detail (header, action buttons, comments, events, runs) re-fetches every 5 s on the same cadence as the board itself, so a worker transition (e.g. running → done elsewhere) is reflected without the user having to close + reopen. The Log tab&#x27;s 2 s poll runs separately and self-cancels the moment the task transitions out of <code>running</code>.</li>
</ul>
<ul>
<li><strong>Inline assignee picker on the inspector header.</strong> The assignee badge is a clickable menu — set means a <code>.brand</code> (rust) chip, unassigned means a <code>.warning</code> (yellow) chip so the eye catches it instantly. Tapping opens a menu of every known profile (union of <code>~/.hermes/profiles/</code>, current task assignees, and the active local profile from <code>HermesProfileResolver</code>) plus an &quot;Unassigned&quot; option. Selection routes through <code>kanban assign</code> and immediately follows with <code>kanban dispatch</code> so the task gets picked up promptly. Solves the &quot;I assigned a profile but nothing happened&quot; gap end-to-end without the user touching a terminal.</li>
</ul>
<ul>
<li><strong>Health banner in the inspector.</strong> Surfaces two conditions that previously left users staring at a stuck task with no explanation. <strong>Yellow</strong> when the task is unassigned in <code>ready</code> / <code>todo</code>: <em>&quot;Won&#x27;t run automatically — Hermes&#x27;s dispatcher silently skips tasks with no assignee.&quot;</em> The dispatcher&#x27;s own <code>--json</code> output literally lists these under <code>skipped_unassigned</code>; we now surface that to the human. <strong>Red</strong> when the most-recently-completed run ended in a non-success outcome (<code>stale_lock</code> / <code>crashed</code> / <code>gave_up</code> / <code>timed_out</code> / <code>spawn_failed</code> / <code>reclaimed</code> / <code>failed</code>): banner displays the outcome label + the raw <code>error</code> field from the run record, so you don&#x27;t have to dig into the Runs tab to discover it. The red banner is suppressed while a fresh attempt is running — once status flips back to <code>running</code>, the previous outcome is stale signal and the Log tab&#x27;s live stream is the right thing to look at.</li>
</ul>
<ul>
<li><strong>Card-level signals.</strong> Cards in <code>running</code> get a 2 px <code>ScarfColor.info</code> left edge + a subtle title shimmer so live work is obvious at a glance. Blocked cards get a 2 px <code>ScarfColor.warning</code> left edge + a ⚠ glyph next to the title. Done cards dim to 0.7 opacity in light mode, 0.55 in dark, with a green ✓ in the title row. Cards in <code>ready</code> / <code>todo</code> with no assignee get a yellow ⚠ glyph in the title row with a tooltip explaining the dispatcher won&#x27;t pick them up — same signal as the inspector banner, just at the board level so triage is one keypress away.</li>
</ul>
<ul>
<li><strong><code>Board | List</code> toggle at the top of the route.</strong> The v2.6 read-only list view is preserved in <code>KanbanListView.swift</code> and surfaced via a segmented picker, so users on narrow windows or anyone who prefers a flat sortable list can opt in. Choice persists across launches via <code>@AppStorage</code>.</li>
</ul>
<ul>
<li><strong>New Task sheet</strong> (<a href="scarf/scarf/Features/Kanban/Views/KanbanCreateSheet.swift">KanbanCreateSheet.swift</a>). Title, body (markdown supported), assignee (defaults to <code>HermesProfileResolver.activeProfileName()</code> so newly-created tasks actually run), workspace kind (segmented <code>Scratch / Worktree / Project Dir</code>; locked to Project Dir on per-project boards), priority slider, comma-separated skills with autocomplete from <code>~/.hermes/skills/</code>, optional tenant (hidden on per-project boards — the slug is implicit), and a &quot;Send to triage&quot; toggle. Submit fires <code>kanban create --json</code> and immediately follows with <code>kanban dispatch</code> so an assigned task transitions <code>ready</code> → <code>running</code> within seconds rather than waiting for the gateway dispatcher&#x27;s internal cycle.</li>
</ul>
<ul>
<li><strong>Kanban moved from Manage → Monitor in the sidebar.</strong> It&#x27;s runtime work-in-progress, not configuration. Sits between Activity and the rest of Manage so users see &quot;what&#x27;s happening right now&quot; at a glance.</li>
</ul>
<h4>Per-project Kanban</h4>
<ul>
<li><strong><code>DashboardTab.kanban</code> on every project</strong>, capability-gated on <code>HermesCapabilities.hasKanban</code>. Renders a project-scoped <code>KanbanBoardView</code> filtered to the project&#x27;s tenant slug. Workspace defaults in the New Task sheet are pre-pinned to <code>dir:&lt;project.path&gt;</code>. Empty state explains the project doesn&#x27;t have any tasks yet and offers a &quot;New Task&quot; CTA — the empty board IS the discovery surface.</li>
</ul>
<ul>
<li><strong>Tenant minting via <a href="scarf/scarf/Core/Services/KanbanTenantResolver.swift">KanbanTenantResolver</a>.</strong> Each Scarf project gets a stable <code>scarf:&lt;slug&gt;</code> tenant minted on first kanban interaction and persisted to <code>&lt;project&gt;/.scarf/manifest.json</code> (new optional <code>kanbanTenant</code> field on <code>ProjectTemplateManifest</code>). Slug rules: lowercased, hyphenated, ≤ 48 chars, <code>scarf:</code> prefix to avoid collision with hand-typed tenants. Once minted, the tenant is <strong>immutable across rename</strong> — tasks already on the board carry the original slug, so renaming the project doesn&#x27;t orphan them. Bare projects (no manifest) get a sentinel manifest written with <code>id: scarf/&lt;project-id&gt;</code> + <code>version: 0.0.0</code> + just the <code>kanbanTenant</code> set; the <code>ProjectAgentContextService</code> reader recognizes the sentinel and refuses to surface it as a &quot;Template&quot; line in the AGENTS.md block, so the project doesn&#x27;t suddenly start advertising a fake template to the agent.</li>
</ul>
<ul>
<li><strong>Agent-side tenant injection.</strong> <a href="scarf/scarf/Core/Services/ProjectAgentContextService.swift">ProjectAgentContextService.renderBlock</a> emits a &quot;Kanban tenant&quot; line inside the <code>&lt;!-- scarf-project --&gt;</code> markers in <code>&lt;project&gt;/AGENTS.md</code> whenever a tenant exists, instructing the agent to pass <code>--tenant scarf:&lt;slug&gt;</code> on <code>hermes kanban create</code>. <code>ChatViewModel.startACPSession</code> already calls <code>refresh(for:)</code> before opening every project chat, so the agent reads a fresh tenant on every session start with no extra wiring. Agents are imperfect at flag discipline; a forgotten <code>--tenant</code> lands the task in the global &quot;Untagged&quot; group rather than failing — acceptable v2.7.5 behavior.</li>
</ul>
<ul>
<li><strong><code>kanban_summary</code> dashboard widget</strong> (<a href="scarf/scarf/Features/Projects/Views/Widgets/KanbanSummaryWidgetView.swift">KanbanSummaryWidgetView.swift</a>). New widget kind for project dashboards: shows the top three <code>running</code> / <code>blocked</code> / <code>todo</code> tasks for the project&#x27;s tenant by priority, plus a glance footer (<code>&quot;12 todo · 3 running · 5 blocked&quot;</code>) sourced from <code>kanban stats</code>. Polls every 10 s while the dashboard is foregrounded. Widget vocabulary registered in <a href="tools/widget-schema.json">tools/widget-schema.json</a> and rendered on the catalog site via <a href="site/widgets.js">site/widgets.js</a>; template authors can drop a <code>{ kind: kanban_summary, max_rows: 3 }</code> block into <code>dashboard.json</code>.</li>
</ul>
<h4>iOS / iPadOS</h4>
<ul>
<li><strong>Read-only Kanban tab on <code>ProjectDetailView</code></strong> (<a href="scarf/Scarf%20iOS/Kanban/ScarfGoKanbanView.swift">Scarf iOS/Kanban/ScarfGoKanbanView.swift</a>). Same five-column collapse rendered as a horizontally-paged segmented <code>Picker</code> of single-column lists — HIG-friendly on iPhone where a 5-column grid forces unreadable card widths. Pulls live status, assignee, workspace, skills, priority chips. Tap a card → modal <code>NavigationStack</code> detail sheet (<a href="scarf/Scarf%20iOS/Kanban/ScarfGoKanbanDetailSheet.swift">ScarfGoKanbanDetailSheet.swift</a>) with the same Comments / Events / Runs tabs the Mac inspector has. Read-only in v2.7.5 — mutations + drag-drop on iPad land in v2.8 once the Mac flow is fully shaken out. Card titles use semantic <code>.headline</code> (not <code>ScarfFont</code>) so Dynamic Type works; chrome (badges) stays on <code>ScarfBadge</code> for fixed visual weight per the project&#x27;s iOS conventions.</li>
</ul>
<h4>ScarfCore</h4>
<ul>
<li><strong><code>KanbanService</code> actor</strong> (<a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Services/KanbanService.swift">Packages/ScarfCore/Sources/ScarfCore/Services/KanbanService.swift</a>) — pure-I/O Sendable actor wrapping every Hermes v0.12 verb (<code>list / show / runs / stats / assignees / create / assign / claim / comment / complete / block / unblock / archive / dispatch / link / unlink / log</code>). Dispatches each CLI invocation through <code>Task.detached(priority: .utility)</code> matching the existing concurrency conventions. Errors land in <a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Models/KanbanError.swift">KanbanError</a> and surface as inline banners (not modal alerts) since the board is high-frequency. The &quot;no matching tasks&quot; stdout sentinel is normalized to <code>[]</code> rather than thrown.</li>
</ul>
<ul>
<li><strong>Pure transition planner.</strong> <code>KanbanService.plan(for: KanbanTransition)</code> is a synchronous function that maps a <code>(from, to)</code> column pair to the right verb sequence — <code>(.upNext, .running) → [.dispatch]</code>, <code>(.blocked, .running) → [.unblock, .dispatch]</code>, etc. Disallowed transitions throw <code>KanbanError.forbiddenTransition</code> with a user-actionable reason. The planner is fully tested in <code>KanbanModelsTests.swift</code>. Critically: <code>dispatch</code> (not <code>claim</code>) is the verb used for Up-Next → Running. Hermes&#x27;s <code>claim</code> is documented as &quot;manual alternative to the dispatcher&quot; and assumes the caller spawns the worker themselves — Scarf doesn&#x27;t, so calling <code>claim</code> from drag-drop reserved tasks but never spawned work, and the dispatcher reclaimed them ~15 minutes later (<code>stale_lock</code>). <code>dispatch</code> is the right primitive for a GUI client.</li>
</ul>
<ul>
<li><strong>Cross-platform <a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Services/KanbanTenantReader.swift">KanbanTenantReader</a>.</strong> Read-only projection over <code>&lt;project&gt;/.scarf/manifest.json</code>&#x27;s <code>kanbanTenant</code> field. The full <code>ProjectTemplateManifest</code> type lives in the Mac target; this lightweight reader gives iOS a way to filter the per-project board by tenant without linking the full manifest model.</li>
</ul>
<ul>
<li><strong>Timestamp decoding tolerates both shapes.</strong> Hermes emits <code>created_at</code> / <code>started_at</code> / <code>completed_at</code> / <code>last_heartbeat_at</code> etc. as Unix integer seconds (its SQLite columns are INTEGER), but earlier wire docs implied ISO-8601 strings. The decoder now accepts either an integer or a string and normalizes to ISO-8601 so downstream code only handles one type. Locked in by <code>decodeUnixIntegerTimestamps</code> in <code>KanbanModelsTests</code>.</li>
</ul>
<ul>
<li><strong><code>KanbanBoardViewModel</code> optimistic merge.</strong> Holds <code>optimisticOverrides: [taskId: status]</code> for in-flight drags; the polled response merges with optimistic state until the server confirms the new status, so a stale poll arriving milliseconds after a drop can&#x27;t snap the card back to its old column. On CLI failure the override is removed and the message lands in the inline banner.</li>
</ul>
<h3>Dispatch + assignee fixes</h3>
<p>A diagnostic round driving real tasks end-to-end exposed a connected bug pattern that the polish pass closed:</p>
<ul>
<li><strong>Hermes&#x27;s dispatcher silently skips unassigned tasks</strong> — its <code>kanban dispatch --json</code> output literally lists them under a <code>skipped_unassigned</code> key and moves on. Tasks created without an assignee sat in <code>ready</code> indefinitely and the user had no signal anything was wrong. The New Task sheet now defaults to the active Hermes profile, the inspector header shows a yellow &quot;Unassigned&quot; chip + warning banner, every <code>ready</code> / <code>todo</code> card without an assignee gets a ⚠ glyph + tooltip, and the inspector&#x27;s inline assignee picker fixes it in one click.</li>
</ul>
<ul>
<li><strong>Drag-to-Running used to call <code>claim</code></strong>, which is a manual alternative to the dispatcher. Status flipped to <code>running</code>, but no worker spawned (Scarf doesn&#x27;t host workers), and 15 minutes later the dispatcher reclaimed the task with a <code>stale_lock</code> outcome. Replaced with <code>dispatch</code> end-to-end so the gateway-running dispatcher actually does the spawning.</li>
</ul>
<ul>
<li><strong><code>hermes kanban assignees</code> empty-state was leaking into the picker.</strong> The CLI prints a literal sentinel <code>(no assignees — create a profile with hermes -p &lt;name&gt; setup)</code> when the table is empty; the parser was tokenizing it on whitespace and offering <code>(no</code> as a profile in the menu. Parser now skips the sentinel, validates each candidate against <code>^[a-zA-Z0-9_-]+$</code>, and falls back cleanly to the active local profile when the table is empty.</li>
</ul>
<ul>
<li><strong><code>spawn_failed</code> from &quot;executable not found on PATH&quot;</strong> — most subtle of the lot. macOS GUI apps inherit a launch-services PATH (<code>/usr/bin:/bin:/usr/sbin:/sbin</code>) that doesn&#x27;t include <code>~/.local/bin</code> (where pipx installs <code>hermes</code>) or <code>/opt/homebrew/bin</code>. Scarf was finding <code>hermes</code> for its own invocation via the absolute-path resolver in <code>HermesPathSet.hermesBinaryCandidates</code>, but when the dispatcher then spawned a worker process, that worker inherited Scarf&#x27;s GUI PATH and couldn&#x27;t find <code>hermes</code> by name — recording an <code>outcome=spawn_failed</code> run with the exact &quot;executable not found on PATH&quot; message. <code>LocalTransport</code> now grows an <code>environmentEnricher</code> static (mirroring <code>SSHTransport.environmentEnricher</code>) wired by <code>scarfApp.swift</code> to the same <code>HermesFileService.enrichedEnvironment()</code> login-shell probe the SSH transport uses. Every local subprocess Scarf spawns now sees the user&#x27;s full PATH and credential env, so a spawned-from-Scarf hermes can spawn its children by name without reaching for absolute paths. Defense-in-depth: <code>subprocessEnvironment(forExecutable:)</code> also unconditionally prepends the executable&#x27;s parent directory to PATH, so the fix works even if the enricher hasn&#x27;t been wired (early startup, tests).</li>
</ul>
<h3>Migrating from 2.7.1</h3>
<p>Sparkle will offer the update automatically. No config migration, no schema changes — <code>~/.hermes/kanban.db</code> is shared across all Hermes clients and Scarf only reads/writes through the documented CLI surface. Existing Scarf projects pick up the new project Kanban tab on first open; the tenant slug is minted lazily on first kanban interaction inside the project, so projects with no kanban activity stay byte-identical until the user opens the tab.</p>
<p>If you have an existing project with a Scarf-managed <code>manifest.json</code>, the new optional <code>kanbanTenant</code> field is added on next mint and lives alongside any template-author config schema without touching it. Templates do not ship <code>kanbanTenant</code> (it&#x27;s user-machine-scoped state); the export pipeline strips it.</p>
<p>If you&#x27;ve been running tasks via the v2.6 read-only list and your Hermes host already runs the gateway dispatcher, your existing kanban tasks should appear on the board automatically — there&#x27;s no migration step. Tasks created without an assignee in v2.6 will now show the yellow &quot;Unassigned&quot; warning until you fix them through the inline picker.</p>
<h3>Known limitations</h3>
<ul>
<li><strong>Within-column reorder is not supported.</strong> Hermes has no <code>update</code> verb and no <code>position</code> column on the tasks table — <code>priority</code> is write-once at create time. Sort order inside each column is <code>priority DESC, created_at DESC</code>, matching the dispatcher&#x27;s actual run order. We considered a client-side ordering sidecar; rejected because the on-screen order would diverge from what runs next, which is worse than no manual order. Will revisit if Hermes ships an <code>update --priority</code> verb.</li>
</ul>
<ul>
<li><strong>No live <code>watch</code> streaming yet.</strong> The board polls every 5 s; the inspector polls detail on the same cadence and the Log tab on a 2 s cadence while running. <code>hermes kanban watch --json</code> event streaming + reconnect-with-backoff lands in v2.8 along with iOS write surfaces.</li>
</ul>
<ul>
<li><strong>No bulk re-tag for legacy NULL-tenant tasks.</strong> Tasks created before this release (assignee or no assignee) appear in the global &quot;Untagged&quot; group on the global board. Hermes has no <code>tenant</code> mutation verb post-create, so retagging would be archive + recreate — too destructive to ship in this release.</li>
</ul>
<h3>Acknowledgements</h3>
<ul>
<li>Driven end-to-end against a fresh local Hermes v0.12.0 install with the gateway dispatcher running. Real bug surface mostly came from doing instead of speculating: the <code>claim</code> vs <code>dispatch</code> distinction, the silent <code>skipped_unassigned</code> behavior, the <code>(no</code> parse leak, the integer-vs-ISO timestamp shape, and the stale &quot;Last run&quot; banner during a fresh attempt all surfaced from driving real tasks and watching what actually happened.</li>
</ul>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.7.5/Scarf-v2.7.5-Universal.zip"
sparkle:edSignature="6QLmonqLdavcU3+u7NE3oYL4Iui4wZZ0r9OWM+kj0uJ3tM32C14N35g7kXmADvo50YONIAmxqfkYWo/AIX70AA=="
length="19346988"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.7.1</title>
<sparkle:version>33</sparkle:version>
<sparkle:shortVersionString>2.7.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Thu, 07 May 2026 10:51:54 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.7.1</h2>
<p>A patch release covering three bug reports filed against 2.7.0, plus follow-up cleanups in the same neighborhood. No data migrations, no UI surface changes — drop-in replacement for 2.7.0 on Mac.</p>
<h3>Bug fixes</h3>
<h4>Mac</h4>
<ul>
<li><strong><a href="https://github.com/awizemann/scarf/issues/77">#77</a> — Sessions screen renders empty even when Dashboard reports sessions exist.</strong> v2.7.0 folded the Sessions tab&#x27;s two SQL queries (sessions list + previews) into a single batched SSH round-trip for perf. The combined wire payload for any user with ~150+ sessions crossed macOS&#x27;s 1664 KB pipe-buffer threshold; without a concurrent reader draining the pipe, the remote <code>sqlite3 -json</code> blocked, the script never finished, our 30-second timeout fired, and the call returned an empty result. <code>SSHScriptRunner</code> now drains stdout/stderr concurrently with the running process via <code>FileHandle.readabilityHandler</code>, so the kernel pipe never fills. Same fix applied to the local-execution path. New regression test pushes 256 KB of synthetic output through the runner and asserts full delivery — would have wedged pre-fix.</li>
</ul>
<ul>
<li><strong><a href="https://github.com/awizemann/scarf/issues/78">#78</a> — Skills &quot;What&#x27;s New&quot; pill contradicts the Updates sub-tab.</strong> The pill at the top of the Skills page was rendering on every sub-tab, including Updates. It counts <strong>local</strong> file deltas since the user last clicked &quot;Mark as seen&quot; (e.g. &quot;18 new&quot; = 18 skills landed on disk that you haven&#x27;t acknowledged), while the Updates body runs <code>hermes skills check</code> to find skills with newer <strong>upstream</strong> versions available — a different concept. Two surfaces using the word &quot;update&quot; for two different things made the screen contradict itself. Two changes: the pill now renders only on the Installed sub-tab (Mac and ScarfGo), and its label says &quot;X <strong>changed</strong> since you last looked&quot; instead of &quot;X updated&quot; so the local-file vocabulary doesn&#x27;t collide with upstream-update vocabulary anywhere on the page.</li>
</ul>
<ul>
<li><strong><a href="https://github.com/awizemann/scarf/issues/79">#79</a> — Skills hub search returns nothing for terms visible in Browse.</strong> With the source picker on &quot;All Sources&quot;, <code>hermes skills search &lt;query&gt;</code> (no <code>--source</code> flag) routes through Hermes&#x27;s centralized index and skips external API sources (skills-sh, github, clawhub, lobehub, well-known) — but Browse still aggregates from those sources, so a skill like <code>honcho</code> would show up in Browse and disappear in search. Same picker, same query, contradictory results. Rather than chase Hermes&#x27;s index gaps, &quot;All Sources&quot; search now means &quot;filter what you can already see&quot;: Scarf caches the most recent Browse payload and runs a client-side substring filter (case-insensitive against name, description, and identifier) against it, instantly. Source-specific searches still shell out to <code>hermes skills search --source &lt;s&gt;</code> for full upstream search semantics. Five new tests cover the filter behavior.</li>
</ul>
<ul>
<li><strong><code>hermesPIDResult()</code> — narrow the Hermes &quot;is it running?&quot; probe to the gateway.</strong> Previously <code>pgrep -f hermes</code>, which matched any process with &quot;hermes&quot; in its argv: chat sessions Scarf itself spawns, <code>hermes -z</code> one-shots, log tails, even the README in an editor. The Dashboard &quot;Hermes is running&quot; badge could read true even when the gateway daemon was down. Tightened to a regex that matches only the gateway shape — <code>python -m hermes_cli.main gateway run …</code> and <code>/path/to/hermes gateway run …</code>. All callers (DashboardViewModel, HealthViewModel, SettingsViewModel, scarfApp, stopHermes) want the gateway PID specifically. Cherry-picked from <a href="https://github.com/awizemann/scarf/pull/76">#76</a> — thanks to <a href="https://github.com/unixwzrd">@unixwzrd</a> for the diagnosis and regex.</li>
</ul>
<ul>
<li><strong><code>HealthViewModel.stopDashboard()</code> — stop the dashboard by port, not <code>pkill -f</code>.</strong> External-instance fallback used to be <code>pkill -f &quot;hermes dashboard&quot;</code>, broad enough to match shell history, log tails, README readers — anything with the substring in its argv. Now <code>lsof -tiTCP:&lt;port&gt; -sTCP:LISTEN</code> resolves the PID actually bound to the dashboard port and only that one process gets <code>SIGTERM</code>. Trusting the port is correct here: Scarf owns the configured port and the user-visible intent is &quot;stop the thing on this port.&quot; Direction cherry-picked from <a href="https://github.com/awizemann/scarf/pull/76">#76</a>; the <code>-c hermes</code> filter from the original was dropped because Hermes installs as a Python shebang script and the kernel COMM is <code>python</code>, not <code>hermes</code> — <code>-c hermes</code> would silently miss every standard install.</li>
</ul>
<h3>Documentation + tooling</h3>
<ul>
<li><strong><code>scripts/local-build.sh</code> + <code>BUILDING.md</code> for contributor builds.</strong> New unsigned single-arch Debug build script for contributors without an Apple Developer account. Detects arm64 / x86_64, verifies xcode-select / xcrun / xcodebuild, probes the Metal toolchain (offers an interactive install on TTY, errors cleanly on CI), resolves Swift packages, builds Debug with signing disabled. Optional one-touch <code>ditto</code> to <code>/Applications/scarf.app</code> on explicit y/N. The canonical Release universal CLI in <code>README.md</code> is unchanged — <code>local-build.sh</code> is an alternative for contributors, not a replacement for the shipping build. Cherry-picked from <a href="https://github.com/awizemann/scarf/pull/76">#76</a>.</li>
</ul>
<ul>
<li><strong><code>BUILDING.md</code> + <code>CONTRIBUTING.md</code> — restored Sonoma compatibility messaging.</strong> The runtime min is <strong>macOS 14.6 (Sonoma)</strong> — that&#x27;s the <code>MACOSX_DEPLOYMENT_TARGET</code> on the main <code>scarf</code> target and is intentional. Build min is <strong>Xcode 16.0</strong> (needed for Swift 6 strict-concurrency features). The legacy CONTRIBUTING.md line had drifted to &quot;Xcode 26.3+ / macOS 26.2+&quot;, which would have steered Sonoma contributors and users away from a build that actually runs on their box. Corrected, with a load-bearing-callout in BUILDING.md so future doc edits don&#x27;t silently raise the floor again.</li>
</ul>
<h3>Migrating from 2.7.0</h3>
<p>Sparkle will offer the update automatically. No config migration, no schema changes. Existing sessions, skills, and projects are untouched.</p>
<p>If you&#x27;ve been working around #77 by collapsing the sidebar or restarting Scarf to repopulate the Sessions list, you can stop — sessions should load reliably now.</p>
<h3>Acknowledgements</h3>
<ul>
<li><a href="https://github.com/bricelb">@bricelb</a> for the three v2.7.0 bug reports (<a href="https://github.com/awizemann/scarf/issues/77">#77</a>, <a href="https://github.com/awizemann/scarf/issues/78">#78</a>, <a href="https://github.com/awizemann/scarf/issues/79">#79</a>) — well-instrumented reproductions including screenshots and environment details made the diagnosis straightforward.</li>
<li><a href="https://github.com/unixwzrd">@unixwzrd</a> for <a href="https://github.com/awizemann/scarf/pull/76">#76</a> — the gateway-pgrep tighten, the <code>pkill -f &quot;hermes dashboard&quot;</code> direction, and the <code>local-build.sh</code> contributor flow are all cherry-picked from that PR.</li>
</ul>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.7.1/Scarf-v2.7.1-Universal.zip"
sparkle:edSignature="P9whuwJ264TN1XLaUNvzQK+yKmK2fiyccgKa6TixK3mkIPyVl2XrwPhyHgoMFOQ/c14l+4sizWolx67BSwXBAg=="
length="18806277"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.7.0</title>
<sparkle:version>32</sparkle:version>
<sparkle:shortVersionString>2.7.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Tue, 05 May 2026 18:47:18 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.7.0</h2>
<p>The biggest release since 2.6.0 — a six-week stretch covering <strong>remote-context performance</strong>, a <strong>new project authoring flow</strong>, <strong>dashboard widgets</strong>, <strong>OAuth resilience</strong>, and a top-to-bottom <strong>performance instrumentation harness</strong> that drove the bulk of the rest. 36 commits, no schema bump, no Hermes capability bump.</p>
<p>The throughline: Scarf got materially faster and more honest on slow remote SSH links, where 30-second sqlite timeouts and silently-empty UI used to be common. The skeleton-then-hydrate pattern, SSH cancellation propagation, and ScarfMon-driven diagnosis are the shape of how that work gets done now.</p>
<hr>
<h3>Remote-context performance — chats and Activity in seconds, not 30s timeouts</h3>
<p>Resuming a chat on a slow remote (a 420ms-RTT droplet, an underprovisioned VPS, a tunnel through 4G) used to fetch the full message column set in one shot, which routinely tripped the 30s SSH timeout on chats with multi-page tool result blobs. The 160-message session was broken; the 30-message session was broken too. Activity didn&#x27;t load at all.</p>
<p>v2.7 introduces a <strong>skeleton-then-hydrate pattern</strong> that bounds the wire payload by what the user actually needs to see RIGHT NOW, then fills in the heavy stuff in the background:</p>
<ul>
<li><strong>Chat skeleton.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesDataService.swift"><code>fetchSkeletonMessages</code></a> selects user + assistant rows only (skips <code>role=&#x27;tool&#x27;</code>) with <code>tool_calls</code> / <code>reasoning</code> / <code>reasoning_content</code> hard-NULLed at the SQL level. Wire payload bounded by conversational text alone — typically a few KB. The chat appears in seconds. Background <code>startToolHydration</code> pages through <code>hydrateAssistantToolCalls</code> in 5-id batches to splice tool calls in. Tool-result CONTENT is <strong>opt-in</strong> via Settings → Display → &quot;Load tool results in past chats&quot; (default off); the inspector pane lazy-fetches per-result content via <code>fetchToolResult(callId:)</code> when you open a card.</li>
<li><strong>Activity skeleton.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesDataService.swift"><code>fetchRecentToolCallSkeleton</code></a> returns metadata-only rows (id + session_id + role + timestamp; everything else NULLed). Activity opens in &lt;1s on remote with placeholder rows; real per-call entries swap in as paged hydration completes. New &quot;Loading tool details…&quot; pill in the page header surfaces hydration progress.</li>
<li><strong>Single-id whale recovery.</strong> When a 5-id batch trips the 30s timeout (one row carries an oversized <code>tool_calls</code> blob — a long Edit&#x27;s args, a big diff), an L1 single-id retry isolates the offending row so the rest of the batch still hydrates. Whale row stays bare; assistant message stays readable.</li>
<li><strong>Lazy tool result loading in the inspector.</strong> Default-off avoids the bulk fetch. When you focus a tool call card, ChatInspectorPane fires <code>loadToolResultIfMissing(callId:)</code> which splices a single result into the message stream without re-fetching anything else.</li>
</ul>
<p>Effect: a 160-message thinking-model session that used to time out at exactly 30s now opens in under 2 seconds with placeholder cards filling in over the next few. Activity loads in 500-800ms.</p>
<h4>SSH cancellation that actually cancels</h4>
<p><code>Task.detached { … }</code> doesn&#x27;t inherit cancellation from the awaiting parent, and <code>Task&lt;…&gt; { … }</code> (unstructured) also drops the signal. Without explicit bridging, cancelling a chat-load Task only unwinds Swift state — the underlying ssh subprocess kept running for the full 30s, pinning a remote sqlite query and a ControlMaster session slot. This produced the &quot;third chat hangs&quot; / &quot;dashboard spins after rapid switching&quot; symptom.</p>
<p>v2.7 wires <code>withTaskCancellationHandler</code> through <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Transport/SSHScriptRunner.swift"><code>SSHScriptRunner.run</code></a> and <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/Backends/RemoteSQLiteBackend.swift"><code>RemoteSQLiteBackend.query</code></a> so parent cancellation reaches the <code>Process</code> and calls <code>proc.terminate()</code> within 100ms. New <code>ssh.cancelled</code> ScarfMon event surfaces this.</p>
<h4>In-flight coalescing for <code>loadRecentSessions</code></h4>
<p>File-watcher deltas during an active stream used to stack 2-3 parallel sessions-list reload tasks (the 500ms <code>scheduleSessionsRefresh</code> debounce only suppresses a pending tick, not one already executing). Subsequent callers now await the in-flight load instead of spawning a parallel SSH subprocess. New <code>mac.loadRecentSessions.coalesced</code> event tracks dedup hits.</p>
<h4>Loading-state UX hardening</h4>
<p>The Mac chat sidebar greys out and disables row taps the moment a session-switch is initiated (synchronously, before <code>client.start()</code> returns), with a floating ProgressView showing the current phase: <strong>&quot;Spawning hermes acp…&quot;</strong> → <strong>&quot;Authenticating…&quot;</strong> → <strong>&quot;Loading session…&quot;</strong> → <strong>&quot;Loading history…&quot;</strong> → <strong>&quot;Ready&quot;</strong>. Pre-fix the sidebar looked engageable while the 5-7 second SSH+ACP boot was still in flight, and the user could queue up a second session-switch behind the first. New <code>isStartingSession</code> flag flips on user click for instant feedback.</p>
<h4>Partial-result + mismatch + pinned-model banners</h4>
<ul>
<li><strong>Partial-result banner.</strong> When the skeleton fetch trips an SSH transport failure (rather than a clean empty result), the chat surfaces &quot;Couldn&#x27;t load full chat history — the connection to <em>server</em> timed out&quot; through the existing <code>acpError</code> triplet, plus forces <code>hasMoreHistory = true</code> so the &quot;Load earlier&quot; affordance shows up. Replaces the pre-fix silent empty transcript.</li>
<li><strong>Model/provider mismatch banner.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/ModelPreflight.swift"><code>ModelPreflight.detectMismatch</code></a> recognizes when <code>model.default</code> carries a <code>&lt;provider&gt;/...</code> prefix that disagrees with <code>model.provider</code> (e.g. <code>anthropic/claude-sonnet-4.6</code> + <code>provider: nous</code> after switching OAuth via Credential Pools). Banner offers one-click fix in either direction.</li>
<li><strong>Pinned-model failure hint.</strong> ACP error classifier now recognizes <code>model_not_found</code> / <code>404 messages</code> / <code>model is not available</code> and surfaces &quot;This session was created with a model the provider no longer offers — start a new chat to use your current model&quot; so the pinned-model failure mode has a clear recovery path.</li>
<li><strong>OAuth-completion provider swap.</strong> After a successful OAuth in Credential Pools, if the just-authed provider differs from <code>model.provider</code>, surface &quot;Switch active provider to <em>name</em>?&quot; with [Switch] / [Keep current] instead of auto-dismissing.</li>
</ul>
<hr>
<h3>New Project from Scratch wizard + Keychain-backed cron secrets</h3>
<p>A <strong>third project entry point</strong> alongside Browse Catalog and Add Existing Project: a wizard that scaffolds a Scarf-standard project skeleton (<code>&lt;project&gt;/.scarf/dashboard.json</code> + AGENTS.md marker block), registers it, and hands off to a chat session that auto-activates the bundled <code>scarf-template-author</code> skill. The skill drives the rest conversationally — widgets, optional config schema, optional cron — and writes the final files itself. Wizard stays minimal because the agent does configuration better than a multi-step form. The skill ships bundled inside <code>Scarf.app/Contents/Resources/BuiltinSkills.bundle/</code> and copies into <code>~/.hermes/skills/</code> on launch (idempotent + version-gated).</p>
<p><strong>Cron + Keychain — <code>$SCARF_&lt;SLUG&gt;_&lt;FIELD&gt;</code> env vars.</strong> Cron prompts that referenced <code>secret</code>-typed config fields used to get the literal <code>keychain://...</code> URI back when reading <code>config.json</code>, producing 401s. v2.7 mirrors resolved Keychain values into <code>~/.hermes/.env</code> under a marker-bounded block keyed by template slug:</p>
<pre><code># scarf-secrets:begin local-news-aggregator
SCARF_LOCAL_NEWS_AGGREGATOR_API_TOKEN=actual-value
SCARF_LOCAL_NEWS_AGGREGATOR_RSS_URL=https://example.com/feed
# scarf-secrets:end local-news-aggregator</code></pre>
<p>Hermes already reloads <code>~/.hermes/.env</code> per cron tick, so credential rotation is automatic — just edit the value in Configuration → next tick sees it. The mirror runs at every state-change point: install, post-install Configuration save, uninstall, &quot;Remove from List&quot;, and on app launch (reconciliation pass over registered projects). Source of truth stays in the Keychain — <code>config.json</code> keeps <code>keychain://</code> URIs unchanged. Mode 0600 enforced on <code>~/.hermes/.env</code>.</p>
<p>Cron prompts now reference these env vars directly:</p>
<pre><code>{
&quot;prompt&quot;: &quot;Use the terminal: curl -sS -H \&quot;Authorization: Bearer $SCARF_LOCAL_NEWS_AGGREGATOR_API_TOKEN\&quot; \&quot;$SCARF_LOCAL_NEWS_AGGREGATOR_RSS_URL\&quot; -o {{PROJECT_DIR}}/.scarf/feed.xml&quot;
}</code></pre>
<p><strong>Migration.</strong> First launch of v2.7 walks the project registry and writes the managed block per schemaful project — automatic. Existing cron prompts you wrote against the old (broken) <code>config.json</code> pattern still need updating: open the cron job in Scarf&#x27;s Cron sidebar and edit the prompt, or ask the agent in chat (&quot;Update my Local News cron job&#x27;s prompt to use the new env var convention&quot;) — the bundled <code>scarf-template-author</code> skill (now v1.1.0) documents the convention with worked examples.</p>
<p>Also fixes <a href="https://github.com/awizemann/scarf/issues/75">#75</a> — <code>_NSDetectedLayoutRecursion</code> on the Configuration form for projects whose form transitioned between stages with different intrinsic heights.</p>
<hr>
<h3>Project dashboards — file-reading widgets, sparklines, typed status</h3>
<p>Five new widget types, project-wide auto-refresh, and a structured error card for unknown widgets. Backwards-compatible — every existing <code>dashboard.json</code> renders byte-identically.</p>
<ul>
<li><strong>Project-wide auto-refresh.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/HermesFileWatcher.swift"><code>HermesFileWatcher</code></a> used to watch each project&#x27;s <code>dashboard.json</code> specifically. v2.7 promotes that to a watch on the entire <code>&lt;project&gt;/.scarf/</code> directory. A <code>markdown_file</code> or <code>log_tail</code> widget pointing at <code>&lt;project&gt;/.scarf/reports/foo.md</code> refreshes the moment a cron job rewrites the file. <strong>By convention, place files the dashboard reads inside <code>.scarf/</code></strong> so the watch picks them up.</li>
<li><strong><code>markdown_file</code></strong> — renders a markdown file from disk through the same <code>MarkdownContentView</code> pipeline used by inline <code>text</code> widgets.</li>
<li><strong><code>log_tail</code></strong> — last <code>lines</code> of a file (default 20, max 200), monospaced, ANSI codes stripped.</li>
<li><strong><code>cron_status</code></strong> — last run / next run / state for one Hermes cron job by <code>jobId</code>, plus a small inline log tail. Read-only — Run/Pause/Resume controls stay on the Cron tab.</li>
<li><strong><code>image</code></strong> — local file (<code>path</code> relative to project root) or remote <code>url</code>. Optional <code>height</code> cap. Useful for matplotlib/Plotly PNGs the cron job generates.</li>
<li><strong><code>status_grid</code></strong> — compact NxM grid of colored cells, one per service / item, with hover labels.</li>
<li><strong><code>stat</code> widget gains inline sparklines.</strong> Optional <code>sparkline: [Number]</code> field. SVG-only render, dozens per dashboard cost nothing.</li>
<li><strong>Typed status badges.</strong> <code>list</code> items and <code>status_grid</code> cells share a typed enum (<code>success</code>, <code>warning</code>, <code>danger</code>, <code>info</code>, <code>pending</code>, <code>done</code>, <code>neutral</code>) with lenient decode for synonyms (<code>ok</code>/<code>up</code> → success, <code>down</code>/<code>error</code> → danger). Unknown strings render as plain text.</li>
<li><strong>Structured widget error card.</strong> Replaces the legacy &quot;Unknown: \&lt;type\&gt;&quot; placeholder with a card surfacing the title, specific reason, and a hint.</li>
<li><strong>Schema mirror.</strong> The widget vocabulary lives once at <a href="https://github.com/awizemann/scarf/blob/main/tools/widget-schema.json"><code>tools/widget-schema.json</code></a>; the catalog validator reads from it and enforces per-type required fields.</li>
</ul>
<hr>
<h3>OAuth resilience + Credential Pools</h3>
<ul>
<li><strong>Daily OAuth keepalive cron.</strong> Prevents Anthropic OAuth refresh tokens from expiring after weeks of inactivity. New cron job <code>[scarf:oauth-keepalive]</code> (managed by Scarf) pings Hermes on a daily cadence; the in-app Refresh All Sessions action mirrors the same path on demand.</li>
<li><strong>Remote re-auth.</strong> Re-authenticating against a remote droplet&#x27;s OAuth provider used to be blocked by the lack of a stdin path through SSHTransport. The OAuth flow now drives a remote <code>hermes auth add</code> correctly with stdin forwarded.</li>
<li><strong>OAuth remove button.</strong> Per-provider remove action in Credential Pools (auth.json edit), with confirmation dialog. Companion auto-refresh of the view when <code>auth.json</code> changes externally (file-watcher).</li>
<li><strong><code>resolve_provider_client</code> error classification.</strong> When an auxiliary task references a provider whose credentials aren&#x27;t loaded, Hermes prints <code>resolve_provider_client: &lt;name&gt; requested but &lt;Display Name&gt; not configured</code> to stderr — pre-fix this surfaced in chat as the opaque <code>-32603 Internal error</code> with no actionable detail. Now classified into a clear hint pointing at Settings → Aux Models.</li>
<li><strong>Aux Tab unknown-task surface.</strong> When <code>config.yaml</code> has an <code>auxiliary.&lt;task&gt;</code> block for a task Scarf doesn&#x27;t know about (newer Hermes added it; Scarf hasn&#x27;t caught up), render it as a plain row with the raw provider/model values instead of dropping it silently.</li>
<li><strong>Credential Pools refresh after OAuth sheet dismiss.</strong> Closing the OAuth sheet after a successful add now refreshes the list immediately instead of leaving the just-added pool hidden until the next file-watcher tick.</li>
</ul>
<hr>
<h3>ScarfMon — performance instrumentation harness</h3>
<p>The diagnostic surface that drove the bulk of the v2.7 perf work. Off by default; signpost-only mode (Instruments-friendly) is free; Full mode (4096-entry in-memory ring buffer + os.Logger) is a click away in Settings → Diagnostics → Performance. Wiki: https://github.com/awizemann/scarf/wiki/Performance-Monitoring</p>
<ul>
<li><strong>Phases 1-3</strong> built the core: dispatcher + ring buffer + 3 backends, chat / transport / sqlite measure points, diagnostic counters for chat-render bursts, finalize-burst dampening.</li>
<li><strong>Tier A + B</strong> added per-feature instrumentation: iOS file watcher, sessions list, model catalog, dashboard widgets, image encoder, message hydration.</li>
<li><strong>Nous picker investigation</strong> localized a 60s + 120s beach-ball to a specific path (Nous catalog <code>readCache</code>), then killed the 120s one with dedupe + 5s timeout.</li>
<li><strong>Tier C catch-up</strong> (this release): instrumented Memory / Skills / Cron / Curator load paths so future captures show how often these tabs cost multiple sequential SFTP RTTs on remote.</li>
<li><strong>Per-call bytes recorded</strong> on transport + sqlite events so captures show payload sizes alongside latencies.</li>
<li><strong><code>mac.emptyAssistantTurn</code> event</strong> documents the Nous quirk where the model returns a thought stream with no body (the bubble looks like Hermes is &quot;still thinking&quot; but the turn already finished).</li>
</ul>
<p>Adding a new measure point is two lines. The harness covers Mac and iOS uniformly. The &quot;Copy as JSON&quot; button exports the ring buffer for paste-into-issue diagnosis.</p>
<hr>
<h3>Other fixes + polish</h3>
<ul>
<li><strong>Sessions sidebar reload debounce</strong> — file-watcher deltas during streaming used to flicker the sessions list. Coalesced into one trailing fetch ~500ms after the last tick.</li>
<li><strong>Session-load pagination + race guard</strong> — switching to a small chat while a larger one is mid-fetch could last-write-wins the small chat away. Three race-checks against <code>self.sessionId</code> prevent the stale fetch from overwriting.</li>
<li><strong>Sessions + previews batched</strong> — two separate SSH calls folded into one <code>queryBatch</code> round trip, halving the round-trips for every sidebar refresh.</li>
<li><strong>Remote SQLite query timeout</strong> bumped 15→30s to better tolerate slow links; in-flight query coalescing dedupes concurrent identical queries.</li>
<li><strong><code>Thread.sleep</code> spin replaced</strong> with a kernel-wait via <code>DispatchGroup</code> for <code>runLocal</code> timeout; under concurrent SSH load the old loop accumulated spin-blocked threads and produced 7-second outliers in <code>loadRecentSessions</code>.</li>
<li><strong>Window position + size</strong> persists across launches.</li>
<li><strong>Sidebar reorder</strong> — Projects promoted to first section; profile chip moved under server name.</li>
<li><strong><code>stop</code> badge suppressed</strong> on metadata footer for normal turn ends (it was firing for every clean completion, looking like an error).</li>
<li><strong>Nous picker search field</strong> + <code>model-picker</code> filter for the long Nous overlay model list.</li>
<li><strong><code>oauth-keepalive</code> cron create</strong> — drop the <code>--silent</code> flag Hermes doesn&#x27;t accept.</li>
<li><strong>Snapshot pipeline rewritten</strong> — replaced the <code>sqlite3 .backup</code>-then-download pipeline with direct SSH-streamed query execution (issue <a href="https://github.com/awizemann/scarf/issues/74">#74</a>). Eliminates the multi-minute snapshot wait on multi-GB state.db files. Companion fix: pre-expand <code>~/</code> in Swift via <code>resolvedUserHome</code> so sqlite3 finds the DB without depending on the remote shell&#x27;s tilde expansion.</li>
<li><strong>Aux nested-YAML parser</strong> — corrected the parser so the unknown-task surface works on remote (was previously dropping aux blocks whose <code>provider:</code> value lived on a separate line).</li>
<li><strong><code>ModelPreflight</code> newline trim bug</strong> — <code>.whitespaces</code> doesn&#x27;t strip newlines; switched both trims to <code>.whitespacesAndNewlines</code> so a stray <code>\n</code> in a hand-edited config.yaml doesn&#x27;t false-positive the mismatch banner.</li>
</ul>
<hr>
<h3>What&#x27;s measured today</h3>
<p>321 ScarfCore tests pass (302 prior + 19 new ModelPreflight). New ScarfMon events documented in the <a href="https://github.com/awizemann/scarf/wiki/Performance-Monitoring">Performance-Monitoring wiki</a>.</p>
<h3>Compatibility</h3>
<ul>
<li>macOS 14+ (unchanged).</li>
<li>Hermes target: still <strong>v2026.4.30 (v0.12.0)</strong>. No new Hermes capability gates added.</li>
<li>Existing <code>dashboard.json</code> files render unchanged.</li>
<li>Existing <code>.scarftemplate</code> bundles install unchanged. Catalog manifest schemaVersion stays at 1/2/3 — no bump.</li>
<li>Existing <code>~/.hermes/.env</code> content is preserved byte-identically — Scarf only writes inside its <code># scarf-secrets:begin &lt;slug&gt;</code> / <code># scarf-secrets:end &lt;slug&gt;</code> regions.</li>
<li>The skeleton-then-hydrate chat loader and SSH cancellation propagation are <strong>Mac-only</strong> in this release; ScarfGo (iOS) keeps its existing chat path.</li>
</ul>
<h3>What&#x27;s deferred</h3>
<ul>
<li><strong>Per-widget data sources + per-widget refresh granularity.</strong> The general &quot;widget points at a typed data source&quot; abstraction is the next-largest win in dashboards but materially expands the model + JS mirror + validator surface. The project-wide watch covers the common cron-driven workflow without it.</li>
<li><strong>Cross-project health digest sidebar rollup.</strong> Counting attention-needed projects across the registry — scoped but didn&#x27;t pull its weight. The typed status enum makes it cheap to add later.</li>
<li><strong>Automatic cron-prompt rewriter on upgrade.</strong> Heuristic rewrites of free-form prompts are risky; the docs + agent-assisted path ships in v2.7. Revisit a &quot;scan + fix&quot; UI in v2.8 if real users miss the migration.</li>
<li><strong>iOS New Project wizard + iOS Keychain-env mirror.</strong> ScarfGo&#x27;s project surface is read-only; the wizard&#x27;s chat-handoff pattern depends on Mac-only ACP plumbing.</li>
<li><strong>iOS skeleton-then-hydrate loaders.</strong> Same data-service surfaces are public, but the iOS chat lifecycle is structured differently. Defer until iOS dogfooding shows the same payload-size pain.</li>
<li><strong>Tier C redesigns (Memory/Skills/Cron/Curator).</strong> Instrumented in v2.7; redesign waits for capture data showing which path actually needs the skeleton-then-hydrate treatment.</li>
</ul>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.7.0/Scarf-v2.7.0-Universal.zip"
sparkle:edSignature="JfHK1sKbP3ubbznXx3uY/a3kY2szkWpTUQmtHJE54Uv970T5PxgIHCTwsimqtZCPepUTv2qK4TRQfqHJBKUmCA=="
length="18793474"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.6.5</title>
<sparkle:version>31</sparkle:version>
<sparkle:shortVersionString>2.6.5</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sun, 03 May 2026 20:20:29 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.6.5/Scarf-v2.6.5-Universal.zip"
sparkle:edSignature="2hYRNX/z+mQ7TjlHfDAiTHiP/Rk1BoJRjjefPvutooiptwOK6EyYq/9crnHYZYe8xvEQTDMmAvsPz4vYz823Cg=="
length="18257858"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.6.0</title>
<sparkle:version>29</sparkle:version>
<sparkle:shortVersionString>2.6.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 01 May 2026 13:48:15 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.6.0/Scarf-v2.6.0-Universal.zip"
sparkle:edSignature="gI1uwJAvmwdlvngLcsSQFtMDsHyI6+DWN23ZjOX/BYmX+BqKu0XLuAXL1tztZV9RF1l5PG+vNVWGnq6ivqVQCQ=="
length="18031081"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.5.2</title>
<sparkle:version>28</sparkle:version>
<sparkle:shortVersionString>2.5.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Wed, 29 Apr 2026 11:47:40 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.5.2/Scarf-v2.5.2-Universal.zip"
sparkle:edSignature="tPgE0ajkNs+OYuYGgB8jVLtY/tGTaUJlrJCf5I5AbwCU2R+Zu08D+pLB17q01A3AgwYfYTLZbnoTG+xO4ys8DQ=="
length="17367761"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.5.1</title>
<sparkle:version>27</sparkle:version>
<sparkle:shortVersionString>2.5.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Mon, 27 Apr 2026 13:38:41 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.5.1/Scarf-v2.5.1-Universal.zip"
sparkle:edSignature="OArax2dY25Q7ZRFYGcviaGmCQCJsIugcBdjTET//mJ4XTT/FnnPQoSTIYaQrsV+mwFZU//G75q9PwPtnNsPiCA=="
length="17090983"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.5.0</title>
<sparkle:version>26</sparkle:version>
<sparkle:shortVersionString>2.5.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sat, 25 Apr 2026 15:42:47 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.5.0/Scarf-v2.5.0-Universal.zip"
sparkle:edSignature="YnHpGMIiL8jyDn3+h8B7Gqzrlz8SXDSyiUXGm9DD6BIkRfYfzi3AVatkxfBLMvoVhlqPGKIhKsB8ybqopgjpCw=="
length="16994785"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.3.0</title>
<sparkle:version>25</sparkle:version>
<sparkle:shortVersionString>2.3.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 24 Apr 2026 01:20:51 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.3.0/Scarf-v2.3.0-Universal.zip"
sparkle:edSignature="vae/hUTU7UOSY/LYU/pt1A9wnbKgvp22+e2peGA/clmloaA22gxCnBX5JALT1w93eHYMLOtvdSf5OrNHyogHDQ=="
length="14783787"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.2.1</title>
<sparkle:version>24</sparkle:version>
<sparkle:shortVersionString>2.2.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Thu, 23 Apr 2026 20:10:10 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.2.1/Scarf-v2.2.1-Universal.zip"
sparkle:edSignature="nFdO9t2wWWKeXehQCm7btr7kzCtmDDg4xsvrm4Z24fqOB+Y9ffYcIBr+e9pqnLSIJJ6r/lYcyz5FlkUMjYXyCw=="
length="17868308"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.2.0</title>
<sparkle:version>23</sparkle:version>
<sparkle:shortVersionString>2.2.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Thu, 23 Apr 2026 16:31:53 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.2.0/Scarf-v2.2.0-Universal.zip"
sparkle:edSignature="mGuKLJbcugMTKdSlrkgYKjpVAaXY8CsMVhCiAo/O7b4K8S/fRaK7ZZNdbLtfSGndrbVWcSU0IIhaB1rBGaPOBQ=="
length="17867934"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.1.0</title>
<sparkle:version>22</sparkle:version>
<sparkle:shortVersionString>2.1.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Tue, 21 Apr 2026 01:50:30 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.1.0/Scarf-v2.1.0-Universal.zip"
sparkle:edSignature="kllR3yC/Cze1W9fSM+WRIE5YVObubEGmV629hAvxzVhvVIJ9n+qa00WOAC3YakZLEKX46DmowEMQf5ikqQGODQ=="
length="17243337"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.0.2</title>
<sparkle:version>21</sparkle:version>
<sparkle:shortVersionString>2.0.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Mon, 20 Apr 2026 22:50:02 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.0.2/Scarf-v2.0.2-Universal.zip"
sparkle:edSignature="3BF7PzLqO835wr+cCEA0Ls4kfp2hNwgDmM8YlvUKM9R/GwTs+tgtSPLTwfr1UyXNH/83uNRuSeZM8iNgV4dPDA=="
length="17063811"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.0.0</title>
<sparkle:version>19</sparkle:version>
<sparkle:shortVersionString>2.0.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sun, 19 Apr 2026 20:11:43 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.0.0/Scarf-v2.0.0-Universal.zip"
sparkle:edSignature="QTgPwFgHk5SbUYKrgg412o/Yc7ZhSiow/33EoWnN+julyEVw/0MwemjHNcwcX+aVQJV4arMp7xGVMzaVV/j1CQ=="
length="16906164"
type="application/octet-stream" />
</item>
<item>
<title>Version 1.6.2</title>
<sparkle:version>18</sparkle:version>
<sparkle:shortVersionString>1.6.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sat, 18 Apr 2026 00:22:28 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v1.6.2/Scarf-v1.6.2-Universal.zip"
sparkle:edSignature="wtf0QjTKCvYq8BZW1meeWdWk8GMsbYopfM/DNiYw8ImdgmX6X8jN5+bIG9KvzYv0VgZ0la8ssSliiz7zdJA2CQ=="
length="16570465"
type="application/octet-stream" />
</item>
<item>
<title>Version 1.6.1</title>
<sparkle:version>17</sparkle:version>
<sparkle:shortVersionString>1.6.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 17 Apr 2026 02:11:53 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v1.6.1/Scarf-v1.6.1-Universal.zip"
sparkle:edSignature="hoYDb7VRQ+YDNUox1kf7eYbhckOJWYLEi8ZPfBZG59qK4L/5N2mmgV7jOCLriHkNx0F4mvM9UK8UQZIkxsX1DA=="
length="16566934"
type="application/octet-stream" />
</item>
</channel>
</rss>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 274 KiB

-684
View File
@@ -1,684 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Scarf — Native Mac &amp; iOS app for your Hermes AI agent</title>
<meta name="description" content="Scarf is the native macOS and iOS GUI for the Hermes AI agent — sessions, projects, memory, skills, cron, multi-server SSH. Free and open source.">
<link rel="canonical" href="https://awizemann.github.io/scarf/">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="Scarf">
<meta property="og:title" content="Scarf — Native Mac &amp; iOS app for your Hermes AI agent">
<meta property="og:description" content="Native macOS and iOS GUI for the Hermes AI agent. Sessions, projects, memory, skills, cron, multi-server SSH. Free and open source.">
<meta property="og:url" content="https://awizemann.github.io/scarf/">
<meta property="og:image" content="https://awizemann.github.io/scarf/assets/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Scarf — native Mac & iOS app for the Hermes AI agent">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Scarf — Native Mac &amp; iOS app for your Hermes AI agent">
<meta name="twitter:description" content="Native macOS and iOS GUI for the Hermes AI agent. Sessions, projects, memory, skills, cron, multi-server SSH.">
<meta name="twitter:image" content="https://awizemann.github.io/scarf/assets/twitter-card.png">
<!-- Theme + favicons -->
<meta name="theme-color" content="#C2563D" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1A1818" media="(prefers-color-scheme: dark)">
<link rel="icon" href="favicon.ico" sizes="any">
<link rel="icon" href="assets/icon-192.png" type="image/png" sizes="192x192">
<link rel="icon" href="assets/icon-512.png" type="image/png" sizes="512x512">
<link rel="apple-touch-icon" href="apple-touch-icon.png">
<link rel="manifest" href="manifest.webmanifest">
<!-- AI crawler hint -->
<link rel="alternate" type="text/plain" title="LLM-friendly summary" href="llms.txt">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header" role="banner">
<a class="brand" href="./" aria-label="Scarf home">
<img src="assets/scarf-icon-512.png" width="32" height="32" alt="" decoding="async">
<span class="brand-name">Scarf</span>
</a>
<nav class="site-nav" aria-label="Primary">
<a href="#features">Features</a>
<a href="#ios">iPhone</a>
<a href="templates/">Templates</a>
<a href="#download">Download</a>
<a href="https://github.com/awizemann/scarf" rel="noopener">GitHub</a>
</nav>
<button type="button" class="theme-toggle" aria-label="Toggle dark mode" data-theme-toggle>
<svg class="theme-icon icon-sun" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path></svg>
<svg class="theme-icon icon-moon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
</header>
<main id="main">
<!-- Hero -->
<section class="hero" id="hero" aria-labelledby="hero-title">
<div class="hero-copy">
<h1 id="hero-title">Native Mac &amp; iOS app<br>for your Hermes AI agent.</h1>
<p class="hero-lede">
Scarf is a native macOS and iOS GUI for the
<a href="https://github.com/hermes-ai/hermes-agent" rel="noopener">Hermes AI agent</a>.
See every session, project, skill, memory file, and cron job — locally on your Mac
and remotely from your iPhone over SSH.
</p>
<div class="cta-row">
<a class="btn btn-primary" href="https://github.com/awizemann/scarf/releases/latest" rel="noopener">
<span>Download for Mac</span>
<span class="btn-meta">macOS 14.6+ · Apple Silicon &amp; Intel</span>
</a>
<a class="btn btn-secondary" href="https://testflight.apple.com/join/qCrRpcTz" rel="noopener">
<span>Get on iPhone</span>
<span class="btn-meta">TestFlight · iOS 18+</span>
</a>
</div>
<p class="hero-prereq">
Requires <a href="https://github.com/hermes-ai/hermes-agent" rel="noopener">Hermes</a> installed at <code>~/.hermes/</code>.
</p>
</div>
<div class="hero-visual" aria-hidden="false">
<picture class="hero-mac">
<source media="(prefers-color-scheme: dark)" srcset="assets/screenshots/mac-hero-dark.png">
<img data-dark-src="assets/screenshots/mac-hero-dark.png" src="assets/screenshots/mac-hero.png"
alt="Scarf on macOS — chat view streaming a response with a tool-call card"
width="1600" height="1000"
fetchpriority="high" decoding="async">
</picture>
<picture class="hero-iphone">
<img src="assets/screenshots/ios-chat.png"
alt="ScarfGo on iPhone — chat with the Hermes agent over SSH"
width="430" height="932"
decoding="async">
</picture>
</div>
</section>
<!-- Trust strip -->
<aside class="trust-strip" aria-label="At a glance">
<ul>
<li>Native Swift 6</li>
<li>No Electron</li>
<li>MIT licensed</li>
<li>Sparkle auto-updates</li>
<li>macOS 14.6+ &nbsp;·&nbsp; iOS 18+</li>
</ul>
</aside>
<!-- What Scarf does (AEO load-bearing paragraph) -->
<section class="what" id="what" aria-labelledby="what-title">
<h2 id="what-title">What Scarf does</h2>
<p>
Scarf is a native macOS and iOS GUI for the Hermes AI agent. It surfaces every part of a
running Hermes installation — chat sessions, project workspaces, memory files, installed
skills, MCP servers, cron jobs, messaging gateways, logs, and configuration — through a
sidebar-driven app on Mac and a tab-based companion on iPhone. Scarf reads from
<code>~/.hermes/state.db</code> directly (read-only), streams agent replies in real time
over the Agent Client Protocol, and connects to remote Hermes installations through your
existing SSH config. There is no telemetry, no login, and no separate cloud service.
</p>
</section>
<!-- Mac feature blocks -->
<section class="features" id="features" aria-labelledby="features-title">
<h2 id="features-title" class="section-heading">Built for the way Hermes actually works</h2>
<article class="feature feature-flip">
<div class="feature-text">
<h3>Live agent sessions</h3>
<p>
Real-time streaming chat over the Agent Client Protocol. Tool calls render inline with
collapsible argument and output panes. Tool-permission requests surface as interactive
dialogs you can approve, deny, or stage. Reasoning and thinking blocks display when the
model emits them. Resume any prior session from the sidebar, or jump back into a
conversation that disconnected — Scarf reconnects to the same session id.
</p>
</div>
<div class="feature-visual">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/screenshots/mac-chat-dark.png">
<img data-dark-src="assets/screenshots/mac-chat-dark.png" src="assets/screenshots/mac-chat.png"
alt="Mac app — chat view with a tool-call card and a streaming response"
width="1600" height="1000" loading="lazy" decoding="async">
</picture>
</div>
</article>
<article class="feature">
<div class="feature-text">
<h3>Project workspaces</h3>
<p>
Per-project chat with the agent's working directory pinned, custom dashboards rendered
from a JSON spec the agent itself can author, project-scoped slash commands defined as
Markdown files, and a Scarf-managed <code>AGENTS.md</code> block that gives Hermes
project context before every session boots. Templates package these into a single
<code>.scarftemplate</code> bundle you can share or install with one click.
</p>
</div>
<div class="feature-visual">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/screenshots/mac-projects-dark.png">
<img data-dark-src="assets/screenshots/mac-projects-dark.png" src="assets/screenshots/mac-projects.png"
alt="Mac app — project dashboard with stat boxes and a chart widget"
width="1600" height="1000" loading="lazy" decoding="async">
</picture>
</div>
</article>
<article class="feature feature-flip">
<div class="feature-text">
<h3>Multi-server over SSH</h3>
<p>
One window per Hermes server. Local <code>~/.hermes/</code> is synthesized
automatically; remote servers connect through your existing
<code>~/.ssh/config</code>, ssh-agent, ProxyJump, and ControlMaster. File I/O routes
through scp/sftp; the SQLite database is served from atomic snapshots; chat tunnels as
<code>ssh -T host hermes acp</code> with JSON-RPC end-to-end. There is no companion
service in the middle.
</p>
</div>
<div class="feature-visual">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/screenshots/mac-sessions-dark.png">
<img data-dark-src="assets/screenshots/mac-sessions-dark.png" src="assets/screenshots/mac-sessions.png"
alt="Mac app — sessions browser filtered to a remote server"
width="1600" height="1000" loading="lazy" decoding="async">
</picture>
</div>
</article>
<article class="feature">
<div class="feature-text">
<h3>Memory, skills, MCP</h3>
<p>
Edit <code>MEMORY.md</code> and <code>USER.md</code> with live file-watcher refresh and
external-provider awareness. Browse the Skills Hub across six registries, install,
update, and inspect <code>SKILL.md</code> frontmatter. Configure MCP servers from a
curated preset list (GitHub, Linear, Notion, Sentry, Stripe) or fully custom; test the
connection in-app and watch the discovered tool list populate.
</p>
</div>
<div class="feature-visual">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/screenshots/mac-mcp-dark.png">
<img data-dark-src="assets/screenshots/mac-mcp-dark.png" src="assets/screenshots/mac-mcp.png"
alt="Mac app — MCP servers configuration view"
width="1600" height="1000" loading="lazy" decoding="async">
</picture>
</div>
</article>
<article class="feature feature-flip">
<div class="feature-text">
<h3>Cron &amp; messaging gateways</h3>
<p>
Create, edit, pause, resume, run-now, and delete cron jobs without touching the CLI —
with human-readable schedules ("Every weekday at 9:00 AM") and the underlying
expression a tap away. Native GUI for thirteen messaging platforms (Telegram, Discord,
Slack, WhatsApp, Signal, iMessage, Email, Matrix, Mattermost, Feishu, Home Assistant,
Webhook, CLI) with per-platform credential forms and connectivity dots.
</p>
</div>
<div class="feature-visual">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/screenshots/mac-cron-dark.png">
<img data-dark-src="assets/screenshots/mac-cron-dark.png" src="assets/screenshots/mac-cron.png"
alt="Mac app — cron manager with paused and active jobs"
width="1600" height="1000" loading="lazy" decoding="async">
</picture>
</div>
</article>
<article class="feature">
<div class="feature-text">
<h3>Insights &amp; health</h3>
<p>
Token usage and cost broken down by day, model, and platform; reasoning tokens
tracked separately. Activity heatmaps and notable sessions across 7, 30, 90 days, or
all time. Component-level health checks, on-demand diagnostics, and a one-click debug
report uploader for Hermes support — with a confirmation dialog before anything
leaves the machine.
</p>
</div>
<div class="feature-visual">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/screenshots/mac-dashboard-dark.png">
<img data-dark-src="assets/screenshots/mac-dashboard-dark.png" src="assets/screenshots/mac-dashboard.png"
alt="Mac app — dashboard with token usage, recent sessions, and health"
width="1600" height="1000" loading="lazy" decoding="async">
</picture>
</div>
</article>
</section>
<!-- iOS / ScarfGo -->
<section class="ios" id="ios" aria-labelledby="ios-title">
<div class="ios-copy">
<p class="eyebrow">ScarfGo for iPhone</p>
<h2 id="ios-title">Your Hermes agent, on your phone.</h2>
<p>
ScarfGo is the native iOS companion to Scarf. It speaks SSH directly to your Hermes
host using <a href="https://github.com/orlandos-nl/Citadel" rel="noopener">Citadel</a> — a
pure-Swift SSH stack, no companion service, no developer-controlled relay. Generate an
Ed25519 keypair on the device, paste the public half into <code>authorized_keys</code>,
and you have full project-aware chat, session resume, memory editing, cron browsing,
and skill management on iOS 18+.
</p>
<ul class="ios-points">
<li>Pure-Swift SSH — keys live in the iOS Keychain and never leave the device.</li>
<li>Multi-server: connect to as many Hermes hosts as you have SSH access to.</li>
<li>Project-scoped chat writes the same Scarf-managed <code>AGENTS.md</code> block as Mac.</li>
<li>Same session attribution, so "which project does this conversation belong to?" matches across devices.</li>
</ul>
<a class="btn btn-primary" href="https://testflight.apple.com/join/qCrRpcTz" rel="noopener">
<span>Join the public TestFlight</span>
<span class="btn-meta">iOS 18+</span>
</a>
</div>
<div class="ios-gallery" role="list" aria-label="ScarfGo screenshots">
<div class="phone-frame" role="listitem">
<img src="assets/screenshots/ios-servers.png" alt="ScarfGo — server picker with two configured Hermes hosts" loading="lazy" decoding="async" width="430" height="932">
</div>
<div class="phone-frame" role="listitem">
<img src="assets/screenshots/ios-chat.png" alt="ScarfGo — chat with the Hermes agent" loading="lazy" decoding="async" width="430" height="932">
</div>
<div class="phone-frame" role="listitem">
<img src="assets/screenshots/ios-project-dashboard.png" alt="ScarfGo — project dashboard" loading="lazy" decoding="async" width="430" height="932">
</div>
<div class="phone-frame" role="listitem">
<img src="assets/screenshots/ios-skills.png" alt="ScarfGo — skills browser" loading="lazy" decoding="async" width="430" height="932">
</div>
<div class="phone-frame" role="listitem">
<img src="assets/screenshots/ios-system.png" alt="ScarfGo — system tab" loading="lazy" decoding="async" width="430" height="932">
</div>
</div>
</section>
<!-- Why native -->
<section class="why" id="why" aria-labelledby="why-title">
<h2 id="why-title" class="section-heading">Why a native app</h2>
<div class="why-grid">
<article class="why-card">
<h3>Native, not Electron</h3>
<p>
One Mach-O binary. SwiftUI on macOS 14.6+ and iOS 18+. Kilobytes of memory and
negligible energy use compared to a bundled Chromium. Drag-and-drop, sharing, the
menu bar, Spotlight, and accessibility all behave the way a Mac or iPhone user
expects.
</p>
</article>
<article class="why-card">
<h3>Read-only safe</h3>
<p>
Scarf opens <code>~/.hermes/state.db</code> in read-only WAL mode. The app cannot
corrupt your Hermes data even if it crashes mid-write — because it never writes.
Memory files and cron jobs are the only mutable surfaces, both with explicit
confirmations.
</p>
</article>
<article class="why-card">
<h3>Open and inspectable</h3>
<p>
<a href="https://github.com/awizemann/scarf" rel="noopener">MIT licensed</a>, pure
Swift, zero external runtime dependencies. Build it yourself with
<code>xcodebuild</code> in two minutes. Sparkle auto-updates ship signed,
notarized, EdDSA-verified zips — never silent over-the-wire mutations.
</p>
</article>
</div>
</section>
<!-- Templates teaser -->
<section class="templates" id="templates" aria-labelledby="templates-title">
<h2 id="templates-title">Project templates</h2>
<p>
A <code>.scarftemplate</code> bundle packages a project's dashboard, skills, cron jobs,
memory blocks, slash commands, and configuration schema into one shareable file.
Browse the public catalog and install with a single click.
</p>
<a class="btn btn-secondary" href="templates/">Browse the template catalog →</a>
</section>
<!-- Download -->
<section class="download" id="download" aria-labelledby="download-title">
<h2 id="download-title" class="section-heading">Download</h2>
<div class="download-grid">
<article class="download-card">
<h3>Scarf for Mac</h3>
<p class="download-meta">macOS 14.6 (Sonoma) or later · Apple Silicon &amp; Intel · Universal binary</p>
<ul class="download-points">
<li>Notarized, code-signed Developer ID build</li>
<li>Sparkle auto-updates with EdDSA signature verification</li>
<li>Free and open source under the MIT license</li>
</ul>
<a class="btn btn-primary" href="https://github.com/awizemann/scarf/releases/latest" rel="noopener">
<span>Get the latest release</span>
<span class="btn-meta">GitHub Releases · .zip</span>
</a>
</article>
<article class="download-card">
<h3>ScarfGo for iPhone</h3>
<p class="download-meta">iOS 18.0 or later · iPhone · public TestFlight</p>
<ul class="download-points">
<li>Pure-Swift SSH — no companion service required</li>
<li>Multi-server support, identical to Mac</li>
<li>Free and open source under the MIT license</li>
</ul>
<a class="btn btn-primary" href="https://testflight.apple.com/join/qCrRpcTz" rel="noopener">
<span>Join the TestFlight</span>
<span class="btn-meta">Apple TestFlight</span>
</a>
</article>
</div>
<p class="download-prereq">
Both apps require <a href="https://github.com/hermes-ai/hermes-agent" rel="noopener">Hermes</a>
installed at <code>~/.hermes/</code> on each host you want to manage. Scarf does not include
Hermes itself — see the
<a href="https://github.com/hermes-ai/hermes-agent#installation" rel="noopener">Hermes installation
guide</a> first.
</p>
</section>
<!-- FAQ -->
<section class="faq" id="faq" aria-labelledby="faq-title">
<h2 id="faq-title" class="section-heading">Frequently asked questions</h2>
<div class="faq-list">
<details>
<summary>What is Scarf?</summary>
<div>
<p>Scarf is a native macOS and iOS GUI for the <a href="https://github.com/hermes-ai/hermes-agent" rel="noopener">Hermes AI agent</a>. It surfaces Hermes's sessions, projects, memory, skills, MCP servers, cron jobs, messaging gateways, logs, and configuration through a sidebar-driven Mac app and a tab-based iPhone companion called ScarfGo.</p>
</div>
</details>
<details>
<summary>Do I need Hermes installed first?</summary>
<div>
<p>Yes. Scarf is a client for an existing Hermes installation. It expects to find Hermes's data directory at <code>~/.hermes/</code> on each host you connect to (local or remote). Install Hermes first by following the <a href="https://github.com/hermes-ai/hermes-agent#installation" rel="noopener">Hermes installation guide</a>.</p>
</div>
</details>
<details>
<summary>Does Scarf work without internet?</summary>
<div>
<p>Yes for the local Hermes case — Scarf reads files and the SQLite database directly from <code>~/.hermes/</code> with no network involvement. Internet is only required when Hermes itself reaches out to model providers or MCP servers, when you connect to a remote Hermes host over SSH, or when checking for Sparkle updates.</p>
</div>
</details>
<details>
<summary>Is Scarf open source?</summary>
<div>
<p>Yes. Both Scarf and ScarfGo are <a href="https://github.com/awizemann/scarf/blob/main/LICENSE" rel="noopener">MIT licensed</a> and built from the same open repository at <a href="https://github.com/awizemann/scarf" rel="noopener">github.com/awizemann/scarf</a>. There are no closed-source components and no telemetry.</p>
</div>
</details>
<details>
<summary>What macOS and iOS versions are supported?</summary>
<div>
<p>Scarf for Mac requires macOS 14.6 Sonoma or later, on Apple Silicon or Intel. ScarfGo for iPhone requires iOS 18.0 or later. Both are universal builds; there is no separate Apple Silicon download.</p>
</div>
</details>
<details>
<summary>How does ScarfGo connect to my Mac?</summary>
<div>
<p>ScarfGo speaks SSH directly to your Hermes host using a pure-Swift SSH stack (Citadel). On first launch it generates an Ed25519 keypair on the device — the private key lives in the iOS Keychain (with <code>kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly</code>) and is excluded from iCloud sync. Paste the public key into the host's <code>~/.ssh/authorized_keys</code>, and ScarfGo can run <code>hermes acp</code> over the SSH session for chat and read the SQLite database for everything else. There is no companion service or developer-controlled relay.</p>
</div>
</details>
<details>
<summary>What data does Scarf collect?</summary>
<div>
<p>None. Scarf has no telemetry, no analytics, no crash reporter, and no account system. The only outbound network connections are to GitHub Releases (when you check for updates via Sparkle), to remote Hermes hosts you explicitly add, and to Hermes's own model providers and MCP servers — all initiated by Hermes, not Scarf.</p>
</div>
</details>
<details>
<summary>Where are my conversations stored?</summary>
<div>
<p>In Hermes's own data directory — <code>~/.hermes/state.db</code> for session history and <code>~/.hermes/sessions/session_*.json</code> for full transcripts. Scarf reads these files but never writes to <code>state.db</code>. ScarfGo reads them through SSH-initiated SQLite snapshots and never caches them locally on the device.</p>
</div>
</details>
<details>
<summary>How do updates work?</summary>
<div>
<p>Scarf for Mac uses <a href="https://sparkle-project.org/" rel="noopener">Sparkle</a> for in-app updates — signed and notarized zips with EdDSA signature verification. The appcast lives at <a href="appcast.xml">awizemann.github.io/scarf/appcast.xml</a>. ScarfGo updates through TestFlight in the usual way until it ships on the App Store.</p>
</div>
</details>
<details>
<summary>Can I use Scarf with a remote or headless Hermes server?</summary>
<div>
<p>Yes — that is one of the main use cases. Add the host through <strong>File → Open Server… → Add Server</strong> on Mac, or tap <strong>Add Server</strong> on the ScarfGo Servers tab. Scarf uses the system SSH config (Mac) or a device-generated key (iOS), so anything reachable through your normal terminal SSH workflow works without extra setup. The remote host needs <code>sqlite3</code> and <code>pgrep</code> on its <code>$PATH</code> and the SSH user needs read access to <code>~/.hermes/</code>.</p>
</div>
</details>
<details>
<summary>What's the difference between Scarf and using Hermes from the terminal?</summary>
<div>
<p>Scarf is strictly additive — it visualizes data Hermes already produces. The terminal CLI (<code>hermes chat</code>, <code>hermes cron</code>, <code>hermes mcp</code>, etc.) remains the source of truth for everything. Scarf gives you live streaming chat with rich tool-call rendering, multi-server windows, project workspaces with custom dashboards, and a one-pane view of skills, MCP servers, cron jobs, and gateways without memorizing CLI subcommands.</p>
</div>
</details>
<details>
<summary>Is there a Windows or Linux version?</summary>
<div>
<p>No. Scarf is built on SwiftUI and AppKit and ships only for Apple platforms. There are no current plans for Windows or Linux ports — the
<a href="https://github.com/hermes-ai/hermes-agent" rel="noopener">Hermes CLI</a> itself works on those platforms, and Scarf can connect to a remote Linux Hermes host from a Mac.</p>
</div>
</details>
</div>
</section>
</main>
<footer class="site-footer" role="contentinfo">
<div class="footer-inner">
<div class="footer-brand">
<img src="assets/scarf-icon-512.png" width="40" height="40" alt="" decoding="async">
<p>Scarf is made by <a href="https://github.com/awizemann" rel="noopener">Alan Wizemann</a>. MIT licensed.</p>
</div>
<nav class="footer-nav" aria-label="Footer">
<div>
<h4>Project</h4>
<ul>
<li><a href="https://github.com/awizemann/scarf" rel="noopener">GitHub</a></li>
<li><a href="https://github.com/awizemann/scarf/wiki" rel="noopener">Wiki</a></li>
<li><a href="https://github.com/awizemann/scarf/releases" rel="noopener">Releases</a></li>
<li><a href="https://github.com/awizemann/scarf/blob/main/LICENSE" rel="noopener">License (MIT)</a></li>
</ul>
</div>
<div>
<h4>Community</h4>
<ul>
<li><a href="templates/">Template catalog</a></li>
<li><a href="https://github.com/awizemann/scarf/discussions" rel="noopener">Discussions</a></li>
<li><a href="https://github.com/awizemann/scarf/issues" rel="noopener">Report a bug</a></li>
<li><a href="https://www.buymeacoffee.com/awizemann" rel="noopener">Buy me a coffee</a></li>
</ul>
</div>
<div>
<h4>Technical</h4>
<ul>
<li><a href="appcast.xml">Sparkle appcast</a></li>
<li><a href="llms.txt">llms.txt</a></li>
<li><a href="https://github.com/hermes-ai/hermes-agent" rel="noopener">Hermes agent</a></li>
<li><a href="https://testflight.apple.com/join/qCrRpcTz" rel="noopener">TestFlight (iOS)</a></li>
</ul>
</div>
</nav>
</div>
</footer>
<!-- Structured data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "SoftwareApplication",
"name": "Scarf",
"alternateName": "Scarf for Mac",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "macOS 14.6 or later",
"description": "Native macOS GUI for the Hermes AI agent. Sessions, projects, memory, skills, MCP servers, cron jobs, messaging gateways, multi-server SSH.",
"url": "https://awizemann.github.io/scarf/",
"downloadUrl": "https://github.com/awizemann/scarf/releases/latest",
"softwareVersion": "2.5.2",
"license": "https://opensource.org/licenses/MIT",
"image": "https://awizemann.github.io/scarf/assets/og-image.png",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"author": {
"@type": "Person",
"name": "Alan Wizemann",
"url": "https://github.com/awizemann"
}
},
{
"@type": "MobileApplication",
"name": "ScarfGo",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "iOS 18.0 or later",
"description": "Native iPhone companion to Scarf. Multi-server Hermes management over SSH, project-aware chat, memory editor, cron browser, skills browser.",
"url": "https://awizemann.github.io/scarf/#ios",
"downloadUrl": "https://testflight.apple.com/join/qCrRpcTz",
"license": "https://opensource.org/licenses/MIT",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"author": {
"@type": "Person",
"name": "Alan Wizemann",
"url": "https://github.com/awizemann"
}
},
{
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is Scarf?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Scarf is a native macOS and iOS GUI for the Hermes AI agent. It surfaces Hermes's sessions, projects, memory, skills, MCP servers, cron jobs, messaging gateways, logs, and configuration through a sidebar-driven Mac app and a tab-based iPhone companion called ScarfGo."
}
},
{
"@type": "Question",
"name": "Do I need Hermes installed first?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Scarf is a client for an existing Hermes installation. It expects to find Hermes's data directory at ~/.hermes/ on each host you connect to. Install Hermes first by following the Hermes installation guide."
}
},
{
"@type": "Question",
"name": "Does Scarf work without internet?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes for the local Hermes case — Scarf reads files and the SQLite database directly from ~/.hermes/ with no network involvement. Internet is only required when Hermes itself reaches out to model providers or MCP servers, when connecting to a remote Hermes host over SSH, or when checking for Sparkle updates."
}
},
{
"@type": "Question",
"name": "Is Scarf open source?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Both Scarf and ScarfGo are MIT licensed and built from the same open repository at github.com/awizemann/scarf. There are no closed-source components and no telemetry."
}
},
{
"@type": "Question",
"name": "What macOS and iOS versions are supported?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Scarf for Mac requires macOS 14.6 Sonoma or later, on Apple Silicon or Intel. ScarfGo for iPhone requires iOS 18.0 or later. Both are universal builds."
}
},
{
"@type": "Question",
"name": "How does ScarfGo connect to my Mac?",
"acceptedAnswer": {
"@type": "Answer",
"text": "ScarfGo speaks SSH directly to your Hermes host using Citadel, a pure-Swift SSH stack. On first launch it generates an Ed25519 keypair on the device — the private key lives in the iOS Keychain and never leaves the phone. Paste the public key into the host's authorized_keys and ScarfGo can run hermes acp over the SSH session for chat and read the SQLite database for everything else. There is no companion service or relay."
}
},
{
"@type": "Question",
"name": "What data does Scarf collect?",
"acceptedAnswer": {
"@type": "Answer",
"text": "None. Scarf has no telemetry, no analytics, no crash reporter, and no account system. The only outbound network connections are to GitHub Releases (Sparkle update checks), remote Hermes hosts you explicitly add, and Hermes's own model providers and MCP servers — all initiated by Hermes, not Scarf."
}
},
{
"@type": "Question",
"name": "Where are my conversations stored?",
"acceptedAnswer": {
"@type": "Answer",
"text": "In Hermes's own data directory — ~/.hermes/state.db for session history and ~/.hermes/sessions/session_*.json for full transcripts. Scarf reads these files but never writes to state.db. ScarfGo reads them through SSH-initiated SQLite snapshots and never caches them locally on the device."
}
},
{
"@type": "Question",
"name": "How do updates work?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Scarf for Mac uses Sparkle for in-app updates — signed and notarized zips with EdDSA signature verification. The appcast lives at awizemann.github.io/scarf/appcast.xml. ScarfGo updates through TestFlight in the usual way until it ships on the App Store."
}
},
{
"@type": "Question",
"name": "Can I use Scarf with a remote or headless Hermes server?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Add the host through File → Open Server… → Add Server on Mac, or Add Server on ScarfGo. Scarf uses the system SSH config (Mac) or a device-generated key (iOS), so anything reachable through normal terminal SSH works without extra setup. The remote host needs sqlite3 and pgrep on its PATH and the SSH user needs read access to ~/.hermes/."
}
},
{
"@type": "Question",
"name": "What's the difference between Scarf and using Hermes from the terminal?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Scarf is strictly additive — it visualizes data Hermes already produces. The terminal CLI remains the source of truth. Scarf gives you live streaming chat with rich tool-call rendering, multi-server windows, project workspaces with custom dashboards, and a one-pane view of skills, MCP servers, cron jobs, and gateways without memorizing CLI subcommands."
}
},
{
"@type": "Question",
"name": "Is there a Windows or Linux version?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. Scarf is built on SwiftUI and AppKit and ships only for Apple platforms. The Hermes CLI itself works on those platforms, and Scarf can connect to a remote Linux Hermes host from a Mac."
}
}
]
}
]
}
</script>
<script src="app.js" defer></script>
</body>
</html>
-65
View File
@@ -1,65 +0,0 @@
# Scarf
> Scarf is a native macOS and iOS GUI for the Hermes AI agent. It surfaces every part of a running Hermes installation — chat sessions, project workspaces, memory files, installed skills, MCP servers, cron jobs, messaging gateways, logs, and configuration — through a sidebar-driven Mac app and a tab-based iPhone companion called ScarfGo.
Both apps are free, MIT licensed, and built from a single open repository. Scarf reads from `~/.hermes/state.db` directly (read-only) and streams agent replies in real time over the Agent Client Protocol. It connects to remote Hermes installations using the host's existing SSH config — no companion service, no telemetry, no account.
## Quick facts
- **Platforms:** macOS 14.6+ Sonoma (Apple Silicon and Intel), iOS 18+
- **License:** MIT
- **Repository:** https://github.com/awizemann/scarf
- **Author:** Alan Wizemann
- **Hermes prerequisite:** https://github.com/hermes-ai/hermes-agent installed at `~/.hermes/`
- **Mac download:** https://github.com/awizemann/scarf/releases/latest
- **iOS download:** https://testflight.apple.com/join/qCrRpcTz (public TestFlight)
- **Auto-updates (Mac):** Sparkle, with EdDSA signature verification
- **Telemetry:** none
## Documentation
- [README](https://github.com/awizemann/scarf/blob/main/README.md): project overview, full feature list, build instructions
- [Wiki](https://github.com/awizemann/scarf/wiki): user guide, architecture, design system reference
- [Wiki — ScarfGo](https://github.com/awizemann/scarf/wiki/ScarfGo): iOS companion details
- [Wiki — ScarfGo Onboarding](https://github.com/awizemann/scarf/wiki/ScarfGo-Onboarding): SSH key setup walkthrough
- [Wiki — Platform Differences](https://github.com/awizemann/scarf/wiki/Platform-Differences): what is and isn't shared between Mac and iOS
- [Releases](https://github.com/awizemann/scarf/releases): release notes for every version
- [License](https://github.com/awizemann/scarf/blob/main/LICENSE): MIT
## Feature surfaces
Mac (sidebar sections):
- **Monitor:** Dashboard, Insights, Sessions Browser, Activity Feed
- **Interact:** Live Chat (ACP streaming + Terminal mode), Memory Viewer/Editor, Skills Browser
- **Configure:** Platforms, Personalities, Quick Commands, Credential Pools, Plugins, Webhooks, Profiles
- **Manage:** Tools, MCP Servers, Gateway Control, Cron Manager, Health, Log Viewer, Settings
- **Project Dashboards:** custom JSON-defined dashboards rendered by Scarf, populated by the agent
- **System:** Hermes process control, menu bar status
iOS (ScarfGo, tabs):
- Servers (multi-host management with pure-Swift SSH)
- Dashboard (stats + recent sessions per server)
- Chat (full ACP, project-scoped)
- Sessions (resume, attribute to projects)
- Memory editor (read/write `MEMORY.md`, `USER.md`)
- Cron (list view, human-readable schedules)
- Skills browser (categories + prereq banners)
- Settings (read-only `config.yaml`)
## Differentiators
- Native SwiftUI, not Electron — single Mach-O binary, kilobytes of memory, full system integration
- Read-only access to `state.db` — Scarf cannot corrupt Hermes data because it never writes
- Multi-server: one window per Hermes host on Mac, multi-server on iOS, all over standard SSH
- Project-scoped chat with Scarf-managed `AGENTS.md` block injected before session boot
- Portable `.scarftemplate` bundles for sharing project setups (dashboards, skills, cron jobs, slash commands)
- Live ACP streaming with rich tool-call rendering, permission dialogs, voice control
- 13 messaging platforms managed in one native UI (Telegram, Discord, Slack, WhatsApp, Signal, iMessage, Email, Matrix, Mattermost, Feishu, Home Assistant, Webhook, CLI)
- Open and inspectable — pure Swift, MIT, no external runtime dependencies
## Optional
- [Templates Catalog](https://awizemann.github.io/scarf/templates/): community-contributed `.scarftemplate` bundles, one-click install
- [Sparkle Appcast](https://awizemann.github.io/scarf/appcast.xml): the auto-update feed (RSS/XML)
-24
View File
@@ -1,24 +0,0 @@
{
"name": "Scarf",
"short_name": "Scarf",
"description": "Native macOS and iOS GUI for the Hermes AI agent.",
"start_url": "./",
"scope": "./",
"display": "browser",
"background_color": "#FAF7F2",
"theme_color": "#C2563D",
"icons": [
{
"src": "assets/icon-192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "any"
},
{
"src": "assets/icon-512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "any"
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
## What's New in 1.6.1
### Auto-updates
Scarf now ships with [Sparkle](https://sparkle-project.org). On launch (and daily thereafter) it checks an EdDSA-signed appcast at [awizemann.github.io/scarf/appcast.xml](https://awizemann.github.io/scarf/appcast.xml). When a new version is available you'll get an in-app update prompt — no more manually downloading zips and dragging into Applications.
You can disable automatic checks or trigger a manual one from **Settings → General → Updates**, the menu bar icon, or the **Scarf → Check for Updates…** menu item.
### Notarized & Developer ID signed
This is the first release that's properly Developer ID signed and notarized by Apple. Gatekeeper accepts it on first launch — no more right-click → Open dance, no more "Scarf cannot be opened because the developer cannot be verified" warnings.
### Fixes
- Chat works correctly when no terminal hermes session is running, and surfaces the real error when it can't reach the agent (b6df…)
### Under the hood
- Tracked `Info.plist` (replacing auto-generation) so signing-relevant keys live in version control
- New `UpdaterService` wraps Sparkle and is injected via SwiftUI `.environment()`
- One-command release pipeline at [scripts/release.sh](https://github.com/awizemann/scarf/blob/main/scripts/release.sh) handles archive → sign → notarize → staple → appcast → GitHub release → tag
---
**Migrating from 1.6.0:** unzip and replace your existing `Scarf.app` in `/Applications`. After this release, future updates install in-place via Sparkle.
+13
View File
@@ -0,0 +1,13 @@
## What's New in 1.6.2
### Fixes
- **No more bogus "missing credentials" banner on Chat.** The orange "No AI provider credentials detected" warning was firing on the Chat tab whenever no session was selected, even for users whose credentials were configured and working. Root cause: the preflight check only inspected `~/.hermes/.env` and shell environment variables, missing the Credential Pools file at `~/.hermes/auth.json` (the in-app flow introduced in 1.6.0) and `api_key:` fields in `config.yaml`. The check now covers all four locations Hermes itself reads at runtime, so if you've added credentials via **Configure → Credential Pools**, the warning stays hidden.
### Polish
- Banner subtitle updated to point users at the in-app Credential Pools flow first, rather than prescribing `.env` edits.
---
**Upgrading from 1.6.1:** Sparkle will offer the update automatically. You can also trigger a check via **Scarf → Check for Updates…** or the menu bar icon.
+58
View File
@@ -0,0 +1,58 @@
## What's New in 2.0
Scarf now manages **multiple Hermes installations** — your local `~/.hermes/` plus any number of remote Hermes instances reached over SSH. Every feature that worked on your Mac now works against a Linux server, a Mac mini on the network, or whatever other host has Hermes installed.
This is a major version bump because the entire service layer was rewritten around a `ServerContext` + `ServerTransport` abstraction, and because the window model changed from single-window-single-server to multi-window-one-server-per-window.
### Multi-server
- **Manage Servers** sheet lets you add, rename, and remove remote servers. Each entry is an SSH target (`user@host`, port, optional identity file, optional `remoteHome` override if your install isn't at `~/.hermes/`).
- Each window is bound to exactly one server. Open a second window via **File → Open Server** → pick a different server, and the two run side-by-side with independent state — chat, dashboards, activity, sessions, the lot.
- The menu bar status icon shows a summary across all registered servers (green hare = any Hermes running anywhere).
- Window-state restoration: quit + relaunch re-opens every window you had open, each reconnected to its bound server.
### Remote over SSH
- **ControlMaster connection pooling** — after the first auth, each remote primitive is a ~5ms tunnel call. Uses the system `ssh`, `scp`, `sftp` so your `~/.ssh/config`, ssh-agent, 1Password/Secretive SSH agents, and ProxyJump all work unchanged.
- **DB access via atomic snapshots** — Scarf runs `sqlite3 .backup` on the remote (WAL-safe, won't corrupt), flips the snapshot out of WAL mode, and pulls it down with `scp`. Snapshots are cached under `~/Library/Caches/scarf/snapshots/<server-id>/` and re-pulled when the file watcher sees a change on the remote's `state.db`.
- **ACP chat over SSH** — the Agent Client Protocol tunnel runs `ssh -T host -- hermes acp`. JSON-RPC over stdio travels end-to-end unmodified, so Rich Chat, streaming, tool calls, permission dialogs, and compression all work against the remote agent identically to local.
- **File watcher** — local uses FSEvents (instant); remote polls `stat` mtime every 3s with ControlMaster keeping the cost bounded. Views auto-refresh on any tick.
- **Cleanup on server-remove** — deleting a remote closes its ControlMaster socket (`ssh -O exit`), prunes its snapshot cache, and invalidates any process-wide caches keyed to its ID. App launch also sweeps orphaned snapshot dirs whose UUIDs are no longer in the registry.
### Chat UX overhaul
All of these were visible bugs during remote dogfooding and are now fixed on both local and remote:
- **No more white-screen flash** on the first message of a session. `RichChatView` used to swap `ContentUnavailableView` out for the message list, which tore down and recreated the entire ScrollView hierarchy. The empty state now lives inside the ScrollView itself.
- **No more scroll-jumping to whitespace** at stream start/finish. Replaced six racing `onChange`-driven scroll calls with SwiftUI's built-in `.defaultScrollAnchor(.bottom)`, which is implemented inside the layout pass and doesn't overshoot LazyVStack content.
- **Resuming a session on a remote now shows its full history.** The DB snapshot is refreshed on session-load — previously it was pulled once on first open and never again, so any messages the remote wrote since launch were invisible.
- **"Continue from last session" surfaces errors** instead of silently doing nothing when SSH is down.
- **Typing into a blank Chat always creates a new session.** Previously it auto-resumed the most recently active session in the DB, which often picked up a cron-spawned session that Hermes had already garbage-collected — producing a silent prompt failure.
- **Failed prompts now explain themselves.** When the agent returns `stopReason: "refusal"`, `"error"`, or `"max_tokens"` with no assistant output, a system message appears under your prompt explaining what happened. No more spinning "Agent working…" forever.
### Correctness — remote SQLite
- The WAL-error spam (`cannot open file at line 51044 of [f0ca7bba1c] — os_unix.c:51044: (2) open(/Users/…/state.db-wal) - No such file or directory`) is gone. `sqlite3 .backup` preserves the source DB's journal mode; the scp'd copy used to try to open a WAL sidecar that doesn't exist. The snapshot script now runs `PRAGMA journal_mode=DELETE` after `.backup` on the remote, and Scarf opens remote snapshots with `file:…?immutable=1` as defense-in-depth.
- **Concurrent snapshot dedupe** — a new `SnapshotCoordinator` actor makes sure that when Dashboard + Sessions + Activity all ask for a fresh snapshot at the same moment (e.g. on a file-watcher tick), only one SSH backup runs; the other callers await the in-flight pull and share the result.
### Under the hood
- New `ServerContext` value type flows through `.environment()` to every view and ViewModel. Every file and process operation routes through `context.makeTransport()``LocalTransport` (`FileManager`, `Process`, FSEvents) or `SSHTransport` (ssh, scp, sftp, mtime polling). The protocol is small enough that each transport is ~400 lines.
- Swift 6 complete-concurrency sweep: ~230 warnings reduced to 1. `ServerContext`, `HermesPathSet`, `ServerTransport`, all service inits, and every value-type accessor are explicitly `nonisolated`. Hand-written `Codable` conformances for the nine types whose synthesized conformances were inferred `@MainActor` by Swift 6's default-isolation rule (`ACPRequest`, `ACPRawMessage`, `GatewayState`, `PlatformState`, `HermesCronJob`, `CronSchedule`, `CronJobsFile`, `AuthFile`, `AuthEntry`).
- ACP cwd now comes from the *remote* `$HOME`, probed once on first connect and cached per server. Previously it passed your local Mac's home path to the ACP adapter, which only worked by coincidence when the remote username matched.
### Compatibility
Hermes v0.10.0 is now verified alongside v0.6v0.9. Scarf builds its session/message `SELECT` columns based on an additive schema detection (`hasV07Schema`), so newer Hermes versions with extra columns don't break queries.
### Migration from 1.6.x
- Sparkle will offer the update automatically. Trigger manually via **Scarf → Check for Updates…** or the menu bar.
- Your local server is synthesized automatically — existing 1.6.x users see "Local" in the server list with no setup needed.
- `servers.json` is created on first add-remote. Location: `~/Library/Application Support/scarf/servers.json`.
- Nothing you configured in 1.6.x (OAuth tokens, credential pools, cron jobs, MCP servers, platform setup) is touched. Those live in `~/.hermes/` and remain the source of truth.
### Known limitations
- Remote file watching is 3s mtime polling (vs. FSEvents on local). If you need sub-second updates on a remote, that's a followup.
- The `session/load` ACP call against an already-deleted session returns success-with-no-body from the Hermes adapter — Scarf now detects the resulting `stopReason: "refusal"` and surfaces it, but the underlying Hermes behavior is an upstream-adapter bug that should also get a proper error response.
+68
View File
@@ -0,0 +1,68 @@
## What's New in 2.0.1
Hotfix for [#19](https://github.com/awizemann/scarf/issues/19) and the related reports from the first day of v2.0: users' remote SSH connections would show a green "Connected" pill but every view (Dashboard, Sessions, Activity, Chat) read as empty / "not running" / "not configured". Three distinct environments reported it — Docker Hermes on a LAN, homelab VM over Tailscale, Ubuntu VPS — and every one was a silent file-access failure on the remote that Scarf wasn't surfacing.
### Errors no longer disappear
Every remote read (`config.yaml`, `gateway_state.json`, `state.db`, `pgrep`) used to silently substitute an empty value on *any* failure — permission denied, missing file, `sqlite3` not installed, connection drop — they all looked identical to the UI. Now:
- Each failure logs a specific warning via `os.Logger` (visible in Console.app under subsystem `com.scarf`).
- The Dashboard shows an orange banner above the stats with the exact error (e.g. "Permission denied reading `~/.hermes/state.db`") and a **Run Diagnostics…** button.
- `HermesDataService` exposes a `lastOpenError` so views can explain *why* state.db couldn't be opened, rather than just rendering zeros.
- Routine "file doesn't exist" cases (optional `skill.yaml` metadata, `gateway_state.json` before Hermes starts, `memories/USER.md` on fresh installs) are detected and **not** logged as warnings — only real errors (permission denied, connection drops, `sqlite3` missing) hit the log. Prevents Console from filling with false-positive noise when directory walks encounter optional files.
### New Remote Diagnostics sheet
Accessible from **Manage Servers → 🩺** per-server button, or by clicking the orange connection pill when Scarf can see the server but can't read Hermes state. Runs fourteen checks in a single SSH session and shows pass/fail for each, plus a targeted hint per failure:
- SSH connectivity and auth
- Remote user identity and `$HOME` resolution
- `~/.hermes` directory existence and readability
- `config.yaml` readable (existence *and* actual read access — the old probe only checked existence)
- `state.db` readable
- `sqlite3` installed on the remote (required for the atomic snapshot Scarf pulls)
- `sqlite3` can actually open `state.db`
- `hermes` binary on the non-login `$PATH` (what runtime uses)
- `hermes` binary on the login `$PATH` (what the Test Connection probe uses)
- `pgrep` available (for the "is Hermes running" check)
One **Copy Full Report** button dumps every check as plain text for bug reports, and a raw-output disclosure panel shows the exact stdout/stderr the remote returned whenever any probe fails — so transport-level problems are self-diagnosing.
The diagnostics script is piped to `/bin/sh -s` on stdin rather than passed as `sh -c <script>` argv. The latter was getting split line-by-line by the remote's login shell (newlines parsed as command separators), which stranded variables set on line 1 in an ephemeral `sh` subprocess that exited before line 2 could use them. Stdin-piping runs the whole script in one `sh` process with variable scope preserved.
### Connection pill gains a "degraded" state
The pill used to be green as long as SSH connected; now after connectivity passes it runs a second-tier check (`test -r $HOME/.hermes/config.yaml`). If that fails, the pill turns **orange** with "Connected — can't read Hermes state" and clicking it opens Remote Diagnostics directly. This is the exact symptom mode in #19, and it's now one click away from a specific answer.
The pill's visual also got a pass: the colored dot is replaced with a state-specific SF Symbol (`checkmark.circle.fill` / `stethoscope` / `arrow.triangle.2.circlepath` / `exclamationmark.triangle.fill`), which reads more like a clickable toolbar tool and doubles as the status signal. No custom pill background anymore — the toolbar's native `.principal` bezel is the frame.
### Auto-suggest the correct `remoteHome` during Add Server
When Test Connection can't find `state.db` at the configured (or default) path, it now also probes the common alternate locations — `/var/lib/hermes/.hermes`, `/opt/hermes/.hermes`, `/home/hermes/.hermes`, `/root/.hermes` — and offers a one-click "Use this" fill if it finds one. Removes the need to know that systemd-installed Hermes lives at `/var/lib/hermes/.hermes` by convention.
### Clearer copy for the `remoteHome` field
The Add Server sheet field is now labeled "Hermes data directory" with a description explaining when you'd override it (systemd service installs, Docker sidecars) and noting that Test Connection auto-suggests.
### README has a new "Remote setup requirements" section
Four concrete prerequisites (SSH, `sqlite3`, `pgrep`, read access to `~/.hermes`) and a troubleshooting paragraph pointing at Remote Diagnostics.
### Migrating from 2.0.0
Sparkle will offer the update automatically. Settings and server list are preserved verbatim — this is purely additive (new diagnostics surface, new error banners, auto-suggest in Test Connection). If you were affected by #19, run Remote Diagnostics after updating; the sheet should pinpoint the specific file access issue and suggest a fix.
### Under the hood
- New types: `RemoteDiagnosticsViewModel`, `RemoteDiagnosticsView`. Both are local to Scarf; no new transport protocol.
- `HermesFileService` gains `loadConfigResult()`, `loadGatewayStateResult()`, `hermesPIDResult()`, `readFileResult()`, `readFileDataResult()` — Result-returning variants that preserve the error. Legacy `loadConfig()` etc. still exist as thin forwarders for callers that don't need diagnostics.
- `HermesDataService.open()` records `lastOpenError` with humanized hints for "sqlite3 not installed", "permission denied", and "file not found" — the three failure modes that produce 90% of issue #19 symptoms.
- `ConnectionStatusViewModel` status enum gains `.degraded(reason:)` between `.connected` and `.error`.
- `TestConnectionProbe` result enum gains `suggestedRemoteHome: String?` carrying any alternate-location hit.
### Known follow-ups (not in 2.0.1)
- `TestConnectionProbe` uses a direct-argv ssh invocation that's functionally correct but fragile (works by accident when split across the login shell). Should be ported to the stdin-pipe pattern the diagnostics sheet now uses.
- Remaining `try?`-swallowed read paths beyond the four Dashboard-surfacing ones — Cron, Memory, Skills, MCP Servers, Platforms still silently render empty on read errors. Same fix pattern applies, low priority.
- `hermesBinaryHint` is only populated when the user clicks Test Connection; if they skip it, ACP chat and CLI calls fall back to bare `hermes` which requires it on the non-interactive PATH (rarely true for `~/.local/bin` installs). The connection-pill's second-tier probe could auto-populate this.
- Docker-host support: when users SSH to a Docker host, `pgrep` and `~/.hermes/` on the host don't see what's inside the container. Needs a `docker exec` wrapping option per server.
+41
View File
@@ -0,0 +1,41 @@
## What's New in 2.0.2
The actual root cause of [#19](https://github.com/awizemann/scarf/issues/19), found and patched by Scarf's first external contributor. v2.0.1 added the diagnostics UI assuming file-perm root cause; v2.0.2 fixes the underlying bug for everyone, regardless of perms.
### macOS Unix domain socket path limit (the real #19)
OpenSSH's ControlMaster multiplexes our bursty stat/cat/cp traffic over one TCP session per host. The socket path is bound by `bind(2)` to a Unix domain socket — and macOS' `sun_path` is **104 bytes including the NUL terminator**.
Scarf's old socket path was `~/Library/Caches/scarf/ssh/<%C>` where `%C` is OpenSSH's 64-char SHA1 hash of `(local user, host, port, remote user)`. For a username like `alex.maksimchuk`, the full path landed at **105 bytes** — one byte over the limit. ssh exited 255 with `unix_listener: path "..." too long for Unix domain socket`. Our `LogLevel=QUIET` flag (set so ACP's line-delimited JSON stays binary-clean) suppressed the diagnostic, and the user just saw "Remote command exited 255" — which the UI rendered as the silent empty-data state every reporter in #19 described.
The fix is to use a much shorter path:
```swift
"/tmp/scarf-ssh-\(getuid())" // ~17 bytes + 64 hash + sep + NUL = ~83 bytes
```
Per-user uid suffix keeps two local users' sockets from colliding in the shared `/tmp`, and 0700 perms on the dir keep them inaccessible to other users.
**Massive thanks to Alex Maksimchuk ([@aliatx2017](https://github.com/aliatx2017)) — Scarf's first external PR contributor — for diagnosing and patching this in [#20](https://github.com/awizemann/scarf/pull/20).** That diagnosis only happened because Alex bothered to read the codebase, reproduce against multiple usernames including a Termux/Android instance, and walk back from the cryptic exit code to the actual `bind()` failure. This release wouldn't exist without that work.
### Hardening on top of the fix
Three additions on top of Alex's patch, layered in via separate commits to keep the original change reviewable:
- **Defensive ownership check on the socket dir.** `/tmp` is world-writable, so a malicious local user could pre-create `/tmp/scarf-ssh-<uid>` and trick Scarf into using a hostile directory (we'd silently fail to chmod it back to 0700, since we wouldn't own it). `ensureControlDir` now uses POSIX `mkdir(0700)` (atomic, sets perms at create time) and on `EEXIST` runs `lstat` to verify the entry is a directory we own with mode 0700 — symlink → refuse, wrong owner → refuse + log to `os.Logger`, wrong mode → repair. Closes the `/tmp` pre-creation hole that's the standard concern for any per-user `/tmp` path.
- **Launch-time sweep of stale sockets.** `ServerRegistry.sweepOrphanCaches` already prunes orphaned snapshot directories on launch; it now also removes ControlMaster socket files older than 30 minutes. Socket basenames are `%C` hashes (not ServerIDs), so we can't keep "still registered" sockets the way the snapshot sweep does — but `ControlPersist` is 600s, so anything older than 30 minutes is guaranteed to be a dead orphan from a crashed master, an unclean app exit, or a server removed while another Scarf instance was holding the dir. Keeps `/tmp/scarf-ssh-<uid>/` from accumulating indefinitely until reboot, while leaving any concurrent Scarf instance's live sockets untouched.
- **Regression test for the path-length invariant.** `scarfTests` was a stub — it now has two tests: one asserting `controlDirPath().utf8.count + 1 + 64 + 1 ≤ 104` (would have caught the original #19 bug in CI), one asserting the path includes the current uid (pins the per-user-isolation invariant against a future "simplification" that drops it).
### v2.0.1 diagnostics work is still useful
The diagnostics sheet, orange "degraded" pill, dashboard error banner, and `remoteHome` auto-suggest from v2.0.1 all still ship — they just turn out not to have been the right diagnosis for the original three reporters. They remain valuable for the *other* connection-failure modes they were designed to surface (missing `sqlite3` on the remote, real permission errors, container/host visibility gaps, custom Hermes data directories). If you upgrade to v2.0.2 and *still* see incomplete data, run Remote Diagnostics from **Manage Servers → 🩺** and the sheet will tell you why.
### Migrating from 2.0.0 / 2.0.1 / draft 2.0.1
Sparkle will offer the update automatically. Settings and server list are preserved verbatim. The first time v2.0.2 connects to a remote, it'll create `/tmp/scarf-ssh-<uid>/` with mode 0700; the old `~/Library/Caches/scarf/ssh/` directory becomes unused (you can delete it manually, or leave it — macOS will sweep it eventually).
The previous v2.0.1 draft download remains available for anyone who already grabbed it — it's still a valid build with the diagnostics work. v2.0.2 is the recommended upgrade path.
### Reporters of #19
@cmalpass, @flyespresso, @maikokan — please grab v2.0.2 and confirm the dashboard populates without needing to run Remote Diagnostics first. If it still doesn't, the diagnostics sheet should now have a much better chance of pinpointing what's left.
+52
View File
@@ -0,0 +1,52 @@
## What's New in 2.1.0
Scarf now speaks seven languages and has a proper slash-command menu in the chat. The language work closes [#13](https://github.com/awizemann/scarf/issues/13) and opens the door for community contributions of additional locales.
### Multi-language support
The UI is now fully translated to **Simplified Chinese, German, French, Spanish, Japanese, and Brazilian Portuguese** on top of the existing English. Scarf respects the system language by default; override per-app from **System Settings → Language & Region → Apps → Scarf**.
- **644 source strings** catalogued. **583 translated per locale** — the remaining ~60 are deliberate fall-throughs to English: proper nouns (Scarf, Hermes, OAuth, MCP, SSH), brand names (Docker, Daytona, Singularity, BlueBubbles), format-only tokens (`%lld`, `·`, `•`), and config-literal placeholders (`my_server`, `npx`, `sk-…`).
- **Locale-aware number and date formatting.** Previous builds hardcoded POSIX-style decimal separators (`$12.34`) and English unit names (`"MB"`, `"K"`, `"M"`). Currency now routes through `.formatted(.currency(code: "USD"))`, byte sizes through `.byteCount(style: .file)`, token counts through `.notation(.compactName)`, and the day-of-week chart through `Calendar.current.shortWeekdaySymbols` — so German users see `15,2 MB`, Japanese users see `15.5万 tokens`, and the activity heatmap starts on the locale's first weekday.
- **Microphone permission prompt localized** — the system dialog that appears the first time you enable voice chat now reads in the user's language.
#### How the translation work shipped
Three stacked PRs to keep each piece independently reviewable, all AI-translated with the bar explicitly set low so native speakers can iterate:
1. **[#22](https://github.com/awizemann/scarf/pull/22) — String Catalog infrastructure.** Added `Localizable.xcstrings` + `InfoPlist.xcstrings`, expanded `knownRegions` with the six new locales, and fixed the locale-aware number formatters mentioned above. No user-visible English-locale change; the groundwork only.
2. **[#24](https://github.com/awizemann/scarf/pull/24) — Audit burn-down.** Swept the codebase for "silently un-localizable" patterns that look fine in Xcode's catalog but leak English at runtime: `Text(cond ? "A" : "B")` routes through the String overload instead of `LocalizedStringKey`, as do `Label(stringVar, systemImage:)`, `.help(stringVar)`, and composite format strings with translatable text suffixes. ~40 sites refactored, covering Chat voice/TTS toggles, Logs pickers, Insights period + day names, MCPServer test result, Profiles, SignalSetup, QuickCommands, ConnectionStatusPill. Without this PR the translations would have landed but ~40 visible strings would still have rendered in English.
3. **[#25](https://github.com/awizemann/scarf/pull/25) — Translations + contributor path.** The six locale JSONs + a 90-line merge script + a "Adding a Language" section in `CONTRIBUTING.md`. The sidebar and Settings tab bar fix also shipped here after smoke-testing revealed they were still missed — `Label(section.rawValue, …)` goes to the String overload just like the audit cases.
#### Contributing a new language
Per-locale source of truth lives in [`tools/translations/<locale>.json`](https://github.com/awizemann/scarf/tree/main/tools/translations). Each entry is a plain `{ "English": "Translation" }` map — keys you omit fall through to English at runtime. Workflow is: fork, drop a JSON, run `python3 tools/merge-translations.py`, open a PR. The full bar is documented in [CONTRIBUTING.md → Adding a Language](https://github.com/awizemann/scarf/blob/main/CONTRIBUTING.md#adding-a-language).
Native-speaker review of the initial six locales is welcome — AI translation gets us most of the way, but idiom and tone are better with someone who actually uses the language. Post a PR against the relevant `<locale>.json` and it'll land as a follow-up.
### Chat slash-command menu
Type `/` in Rich Chat and a floating menu appears above the input with every command the connected agent has advertised via ACP's `available_commands_update`, plus any user-defined `quick_commands:` from `~/.hermes/config.yaml`. ↑/↓ to navigate, Tab or Enter to complete, Esc to dismiss. Commands with argument hints (e.g. `/compress <topic>`) insert a trailing space so you can start typing the argument immediately.
The filter uses pure-prefix match and re-renders on every query — the old menu had a description-fallback filter and a cached child view that together pinned `/help` on-screen regardless of what you typed. The dedicated `/compress` button is hidden once the menu has more than one command; it only surfaces when `/compress` is the single advertised slash command, preserving the v2.0 one-click compression flow for that case.
### Chat UX polish
- **Auto-scroll on send and on completion.** `.defaultScrollAnchor(.bottom)` handles slow streaming fine, but rapid slash-command responses (common once the menu lands) outran the anchor and left the reply off-screen. Now the list explicitly scrolls to the latest message when you submit and again when the prompt finishes.
- **Loading state.** `ChatViewModel.isPreparingSession` is true during Starting / Creating / Loading / Reconnecting. While true, the message list swaps its empty-state placeholder for a spinner — non-blocking, just a view inside the ScrollView.
- **Empty-state centering.** The "Start a new session or resume an existing one" placeholder was positioned with a fixed `.padding(.vertical, 80)` that looked wrong at extreme window sizes. Replaced with Spacers inside `.containerRelativeFrame(.vertical)` so it sits in the true vertical center of the chat pane.
- **Session-load whitespace bug.** Opening a session used to render a blank viewport you'd have to scroll up from — the fix was `LazyVStack``VStack` in `RichChatMessageList`. LazyVStack's estimated row heights were fooling `.defaultScrollAnchor(.bottom)` into overshooting real content; VStack measures every row upfront so the anchor has real heights to work with.
### Under the hood
- **String Catalog build pipeline.** `SWIFT_EMIT_LOC_STRINGS` + `STRING_CATALOG_GENERATE_SYMBOLS` are enabled; keys extract automatically on IDE build. Headless builds use `xcrun xcstringstool sync` to merge the per-source `.stringsdata` files into the catalog (wrapped by [`tools/merge-translations.py`](https://github.com/awizemann/scarf/blob/main/tools/merge-translations.py) when applying JSON translations).
- **New docs.** [`scarf/docs/I18N.md`](https://github.com/awizemann/scarf/blob/main/scarf/docs/I18N.md) covers the catalog setup, the patterns that silently bypass localization (and their fixes), and which strings are intentionally kept verbatim. Anyone adding UI copy should read the "Guardrails when writing new UI code" section to avoid re-introducing the leaks #24 cleaned up.
### Migrating from 2.0.x
Sparkle will offer the update automatically. No config migration needed. The first launch after update picks up the system locale — if you want English even on a non-English macOS, set **System Settings → Language & Region → Apps → Scarf → English**.
### Thanks
- [Onion3](https://github.com/Onion3) for filing [#13](https://github.com/awizemann/scarf/issues/13) back in April. The single-locale ask turned into a six-locale rollout.
- Future translators: if you spot a weird AI translation in your language, open a PR against `tools/translations/<locale>.json`. The bar is explicitly low — we'd rather have a 95%-correct translation shipped and iterated on than hold everything for perfection.
+45
View File
@@ -0,0 +1,45 @@
## What's New in 2.2.0
Scarf projects can now travel. This release introduces **Project Templates** — a shareable `.scarftemplate` bundle format that packages a project's dashboard, agent instructions, skills, and cron jobs into a single file anyone can install with one click from a local file or an `scarf://install?url=…` deep link.
### Project Templates
- **Bundle format: `.scarftemplate`.** A zip archive carrying a `template.json` manifest, the project's dashboard, a required `AGENTS.md` (the [Linux Foundation cross-agent instructions standard](https://agents.md/) — reads natively in Claude Code, Cursor, Codex, Aider, Jules, Copilot, Zed, and more), a README shown in the installer, optional per-agent instruction shims (`CLAUDE.md`, `GEMINI.md`, `.cursorrules`, `.github/copilot-instructions.md`), optional namespaced skills, optional cron job definitions, and an optional memory appendix. Every bundle is agent-portable out of the box.
- **Install preview sheet.** Before anything touches disk, Scarf shows you the exact project directory that will be created, every file inside it, every skill that will be namespaced under `~/.hermes/skills/templates/<slug>/`, every cron job that will be registered (always paused — you enable each one manually), and a live diff of the memory appendix against your existing `MEMORY.md`. The manifest's content claim is cross-checked against the actual zip entries so a bundle can't hide files from the preview.
- **`scarf://install?url=…` deep links.** Register Scarf as the handler for the `scarf` URL scheme so a future catalog site can link one-click installs straight into the app. Only `https://` payloads are accepted; `file://`, `javascript:`, and `http://` are refused on principle. A 50 MB size cap keeps a malicious link from exhausting disk. The URL never auto-installs — the preview sheet is always user-confirmed.
- **Export any project as a template.** Select a project, open the new Templates menu in the Projects toolbar, fill in a handful of fields (id, name, version, description, optional author + category + tags), tick the skills and cron jobs you want to include, optionally drop in a memory snippet, and save. The exporter builds the bundle and you can hand it to anyone.
- **No-overwrite, reversible by design.** Installed templates drop a `<project>/.scarf/template.lock.json` recording exactly what they wrote — every project file, skill path, cron job name, and memory block id. Installing the same template id twice is refused at the preview step so you don't accidentally double-append to `MEMORY.md`. Uninstalling by hand is a matter of deleting the project directory, the skills namespace folder, and any `[tmpl:<id>] …` cron jobs — no hidden state.
- **Safe globals.** Skills install to `~/.hermes/skills/templates/<slug>/<skill-name>/` so they never collide with your own skills. Cron jobs are prefixed with `[tmpl:<id>]` and start paused so nothing unexpected kicks off on install. The installer **never** touches `~/.hermes/config.yaml`, `auth.json`, sessions, or any credential-bearing path.
### Using templates
- **Install from file:** Projects → Templates → *Install from File…*, pick a `.scarftemplate` from disk.
- **Install from URL:** Projects → Templates → *Install from URL…*, paste an https URL.
- **Install from the web:** click any `scarf://install?url=…` link in a browser.
- **Export:** select a project → Projects → Templates → *Export "&lt;name&gt;" as Template…*, fill the form, save.
### Under the hood
- New models in `Core/Models/ProjectTemplate.swift` (manifest, inspection, install plan, lock, errors).
- `Core/Services/ProjectTemplateService.swift` unzips, parses, and validates; `ProjectTemplateInstaller.swift` executes the plan atomically-enough (pre-flights conflicts, then writes); `ProjectTemplateExporter.swift` builds bundles from a live project + selections.
- `Core/Services/TemplateURLRouter.swift` is the process-wide landing pad for `scarf://` URLs so a cold-launch browser click still reaches the install sheet.
- Installer dispatches cron creation via `hermes cron create` (there's no direct Scarf write path for `cron/jobs.json`), then diffs before/after to pause the newly-registered jobs.
- New Swift Testing suites: `ProjectTemplateServiceTests`, `TemplateURLRouterTests`, `ProjectTemplateExportTests`.
### Uninstall
- **One-click uninstall** driven by `template.lock.json`. Right-click any template-installed project in the sidebar → **Uninstall Template…**, or click the uninstall button in the dashboard header. A preview sheet lists every file, cron job, and memory block that will be removed, and every user-created file that will be preserved.
- **User content is never removed.** Files you (or the agent) added to the project dir after install — like a `sites.txt` or `status-log.md` — are detected and listed as "keep" in the preview. The project directory itself is removed only if nothing user-owned is left inside.
- **Clean global state.** The isolated `~/.hermes/skills/templates/<slug>/` namespace is removed wholesale. Tagged cron jobs are removed via `hermes cron remove`. The memory block between the `<!-- scarf-template:<id>:begin/end -->` markers is stripped, leaving the rest of MEMORY.md intact. The project registry entry is removed last.
- **No undo.** v1 uninstall is destructive — to reinstall, run the install flow again.
### Not in this release (planned for v2.3)
- In-app catalog browser backed by a GitHub Pages `templates.json`.
- EdDSA-signed bundles reusing the Sparkle key.
- Template updates (compare installed lock against a newer bundle's version, offer a diff).
- Installing into remote `ServerContext`s (v1 is local-only).
### Migrating from 2.1.x
Sparkle will offer the update automatically. No config migration needed. Existing projects are untouched — templates are additive.
-4
View File
@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://awizemann.github.io/scarf/sitemap.xml
+111
View File
@@ -0,0 +1,111 @@
# Scarf — Architecture
## Pattern: MVVM-Feature
Per project standards, every feature is a self-contained module owning Models, ViewModels, and Views.
```
scarf/
Core/
Services/ Hermes data access (SQLite, file I/O, ACP)
Models/ Plain data structs for Hermes entities
Features/
Dashboard/
Views/ DashboardView
ViewModels/ DashboardViewModel
Sessions/
Views/ SessionsView, SessionDetailView
ViewModels/ SessionsViewModel
Activity/
Views/ ActivityView
ViewModels/ ActivityViewModel
Chat/
Views/ ChatView
ViewModels/ ChatViewModel
Memory/
Views/ MemoryView
ViewModels/ MemoryViewModel
Skills/
Views/ SkillsView
ViewModels/ SkillsViewModel
Cron/
Views/ CronView
ViewModels/ CronViewModel
Logs/
Views/ LogsView
ViewModels/ LogsViewModel
Settings/
Views/ SettingsView
ViewModels/ SettingsViewModel
Navigation/
AppCoordinator.swift
SidebarView.swift
```
## Navigation
`AppCoordinator` is `@Observable` and injected via `.environment()` at the app root. It owns:
- `selectedSection: SidebarSection` — which feature is active
- `selectedSessionID: String?` — drill-down into a session
One `NavigationSplitView` at top level, driven by the coordinator. Leaf views read but never own navigation state.
## Services
### HermesDataService
- Opens `~/.hermes/state.db` read-only via SQLite3 C API
- Queries `sessions` and `messages` tables
- Provides session list, message history, search (FTS5), and aggregate stats
- Polling-based refresh (watches WAL modification time)
### HermesFileService
- Reads config.yaml (simple line parser for the YAML subset we need)
- Reads/writes memory markdown files
- Reads cron jobs.json, gateway_state.json, session JSON files
- Reads skill directory structure
### HermesLogService
- Tails log files using file handle + periodic polling
- Parses log level from line format
### ACPClient
- Spawns `hermes acp` via Foundation `Process`
- Writes JSON-RPC to stdin, reads from stdout
- Streams events: ToolCallStart, ToolCallProgress, AgentMessage, AgentThought
- Manages session lifecycle
### HermesFileWatcher
- Uses `DispatchSource.makeFileSystemObjectSource` on key directories
- Triggers refresh callbacks when Hermes writes new data
## Dependencies
Zero external SPM packages:
- **SQLite**: System `sqlite3` C library (available on macOS, `import SQLite3` not needed — use `libsqlite3`)
- **JSON**: Foundation `JSONDecoder` / `JSONSerialization`
- **YAML**: Custom lightweight parser for flat config structure
- **Markdown**: `AttributedString(markdown:)` (built into Foundation)
- **File watching**: GCD `DispatchSource`
- **Subprocess**: Foundation `Process` + `Pipe`
## Sandbox
Disabled. This app reads directly from `~/.hermes/` which is outside any app sandbox container. The `ENABLE_APP_SANDBOX` build setting is set to `NO`.
## Concurrency
- Swift 6 strict concurrency with `@MainActor` default isolation
- Services use `nonisolated` methods with async/await for I/O
- `@Observable` ViewModels on MainActor, call into nonisolated services
- ACP client runs its read loop on a background task
## Data Flow
```
~/.hermes/state.db ──→ HermesDataService ──→ ViewModels ──→ Views
~/.hermes/config.yaml ──→ HermesFileService ──→ ViewModels ──→ Views
~/.hermes/memories/ ──→ HermesFileService ──→ ViewModels ──→ Views
~/.hermes/logs/ ──→ HermesLogService ──→ ViewModels ──→ Views
hermes acp (subprocess) ──→ ACPClient ──→ ChatViewModel ──→ ChatView
HermesFileWatcher ──→ triggers refresh on all services
```
+169
View File
@@ -0,0 +1,169 @@
# Scarf Project Dashboard Schema
Scarf can render project dashboards from a JSON file. Place a `dashboard.json` file at `.scarf/dashboard.json` in your project root, and register the project in Scarf.
## Registration
Projects are registered in `~/.hermes/scarf/projects.json`:
```json
{
"projects": [
{ "name": "my-project", "path": "/path/to/my-project" }
]
}
```
You can also add projects from the Scarf UI via the Projects section.
## Dashboard File
Create `.scarf/dashboard.json` in your project root:
```json
{
"version": 1,
"title": "My Project",
"description": "Optional description",
"updatedAt": "2026-03-31T14:00:00Z",
"sections": [
{
"title": "Section Name",
"columns": 3,
"widgets": []
}
]
}
```
## Widget Types
### stat — Key metric display
```json
{
"type": "stat",
"title": "Test Coverage",
"value": "87.3%",
"icon": "checkmark.shield",
"color": "green",
"subtitle": "+2.1% from last week"
}
```
- `value`: String or number
- `icon`: SF Symbol name (optional)
- `color`: red, orange, yellow, green, blue, purple, pink, teal, indigo, mint, brown, gray (optional)
- `subtitle`: Secondary text (optional)
### progress — Progress bar
```json
{
"type": "progress",
"title": "Sprint Progress",
"value": 0.73,
"label": "73% complete",
"color": "blue"
}
```
- `value`: Number between 0.0 and 1.0
- `label`: Text below the bar (optional)
- `color`: Named color (optional)
### text — Rich text block
```json
{
"type": "text",
"title": "Release Notes",
"content": "**v2.4.1** — Fixed auth timeout\n\n- Bug fix for session handling",
"format": "markdown"
}
```
- `content`: Text content
- `format`: "markdown" or "plain" (default: plain)
### table — Data table
```json
{
"type": "table",
"title": "Recent Deploys",
"columns": ["Date", "Env", "Status"],
"rows": [
["Mar 30", "prod", "success"],
["Mar 29", "staging", "success"]
]
}
```
### chart — Line, bar, or pie chart
```json
{
"type": "chart",
"title": "Tests Over Time",
"chartType": "line",
"series": [
{
"name": "Passing",
"color": "green",
"data": [
{ "x": "Mon", "y": 142 },
{ "x": "Tue", "y": 145 }
]
}
]
}
```
- `chartType`: "line", "bar", or "pie"
- `series[].color`: Named color (optional)
- For pie charts, each series becomes a slice
### list — Checklist
```json
{
"type": "list",
"title": "TODO Items",
"icon": "checklist",
"items": [
{ "text": "Write tests", "status": "done" },
{ "text": "Update docs", "status": "active" },
{ "text": "Deploy", "status": "pending" }
]
}
```
- `status`: "done" (checkmark), "active" (filled circle), "pending" (empty circle)
### webview — Embedded web browser
```json
{
"type": "webview",
"title": "Project Dashboard",
"url": "http://localhost:8000",
"height": 500
}
```
- `url`: Any URL — local servers, file paths, or remote pages
- `height`: Height in points (optional, default: 400)
When a dashboard includes a webview widget, Scarf adds a tabbed interface: **Dashboard** shows all normal widgets, **Site** displays the web content full-canvas. The webview widget is automatically filtered out of the Dashboard tab's grid layout.
## Agent Instructions
To have your Hermes agent generate a dashboard, include these instructions:
> Analyze the project and create a `.scarf/dashboard.json` file with relevant metrics,
> status indicators, and visualizations. Use the Scarf dashboard schema with sections
> containing stat, progress, text, table, chart, list, and webview widgets. Register the project
> in `~/.hermes/scarf/projects.json` if not already registered.
The agent can update the dashboard file at any time — Scarf watches for changes and re-renders automatically.
+183
View File
@@ -0,0 +1,183 @@
# Hermes Agent — Discovery Notes
## Installation
- Binary: `~/.local/bin/hermes` (symlink to venv wrapper)
- Codebase: `~/.hermes/hermes-agent/` (Python 3.11 venv)
- Version: v0.6.0 (March 30, 2026)
- Runs as daemon process
## What Hermes Does
A self-improving AI agent with tool-calling capabilities:
- Interactive terminal chat with syntax highlighting
- 40+ tools (terminal, file, browser, web, code execution, vision, etc.)
- Autonomous skill creation from complex tasks
- Persistent memory (MEMORY.md + USER.md) with periodic nudges
- Multi-platform messaging gateway (Telegram, Discord, Slack, WhatsApp, Signal, Email)
- Cron scheduler for recurring tasks
- Session persistence in SQLite with FTS5 search
- Subagent delegation for parallel workstreams
- MCP (Model Context Protocol) integration
- ACP (Agent Client Protocol) for IDE integration
## File System Layout
```
~/.hermes/
hermes-agent/ Python codebase (70 directories)
run_agent.py Core agent loop
cli.py Terminal UI
model_tools.py Tool dispatcher
toolsets.py Tool definitions
agent/ Agent internals
tools/ 40+ tool implementations
gateway/ Multi-platform messaging
cron/ Scheduler implementation
hermes_cli/ CLI command handlers
acp_adapter/ Agent Client Protocol server
venv/ Python environment
config.yaml User configuration (8.8 KB)
.env API keys (encrypted)
auth.json OAuth tokens
state.db SQLite session database (WAL mode)
sessions/ JSON conversation snapshots
memories/ MEMORY.md, USER.md
skills/ 29 installed skills across 15+ categories
cron/
jobs.json Scheduled job definitions
output/ Job execution output
logs/
errors.log Application errors
gateway.log Gateway platform logs
gateway_state.json Gateway process lifecycle
```
## SQLite Schema (state.db, version 6)
### sessions table
```sql
id TEXT PRIMARY KEY,
source TEXT, -- 'cli', 'telegram', 'discord', etc.
user_id TEXT,
model TEXT,
model_config TEXT, -- JSON
system_prompt TEXT,
parent_session_id TEXT, -- Session splitting on compression
started_at REAL,
ended_at REAL,
end_reason TEXT,
message_count INTEGER,
tool_call_count INTEGER,
input_tokens INTEGER,
output_tokens INTEGER,
cache_read_tokens INTEGER,
cache_write_tokens INTEGER,
reasoning_tokens INTEGER,
billing_provider TEXT,
billing_base_url TEXT,
billing_mode TEXT,
estimated_cost_usd REAL,
actual_cost_usd REAL,
cost_status TEXT,
cost_source TEXT,
pricing_version TEXT,
title TEXT UNIQUE
```
### messages table
```sql
id INTEGER PRIMARY KEY,
session_id TEXT,
role TEXT, -- 'user' or 'assistant'
content TEXT,
tool_call_id TEXT,
tool_calls TEXT, -- JSON array of tool invocations
tool_name TEXT,
timestamp REAL,
token_count INTEGER,
finish_reason TEXT,
reasoning TEXT,
reasoning_details TEXT,
codex_reasoning_items TEXT
```
### messages_fts (FTS5 virtual table)
Full-text search on message content.
## Session JSON Format
```json
{
"session_id": "YYYYmmdd_HHMMSS_6hexchars",
"model": "claude-haiku-4-5-20251001",
"platform": "cli",
"session_start": "ISO8601",
"last_updated": "ISO8601",
"system_prompt": "...",
"tools": [{"type": "function", "function": {"name": "...", "description": "...", "parameters": {...}}}],
"messages": [
{"role": "user", "content": "..."},
{"role": "assistant", "content": "...", "tool_calls": [
{"id": "call_...", "type": "function", "function": {"name": "terminal", "arguments": "{\"command\": \"...\"}"}}
]}
]
}
```
## Cron Jobs Format
```json
{
"jobs": [{
"id": "12hexchars",
"name": "Job Name",
"prompt": "What to do",
"skills": ["skill-name"],
"schedule": {"kind": "once|cron", "run_at": "ISO8601", "display": "human readable"},
"repeat": {"times": 1, "completed": 0},
"enabled": true,
"state": "scheduled|running|completed",
"deliver": "origin|telegram|discord",
"next_run_at": "ISO8601",
"last_run_at": "ISO8601|null",
"last_error": "string|null"
}]
}
```
## Config Structure (config.yaml)
Key sections: model (default, provider), agent (max_turns, tool_use_enforcement, personalities), terminal (backend, cwd, timeout), memory (enabled, char limits, nudge interval), display (personality, streaming, show_reasoning), platform_toolsets (tools per platform).
## ACP (Agent Client Protocol)
- Entry: `hermes acp` or `python -m acp_adapter.entry`
- Transport: stdio JSON-RPC (not HTTP)
- Lifecycle: initialize() -> new_session()/load_session() -> send messages
- Events emitted: ToolCallStart, ToolCallProgress, AgentMessage, AgentThought, SessionUpdate
- Tool kinds: read, edit, execute, fetch, search, think, other
- Tool call IDs: `tc-{uuid.hex[:12]}`
## Log Format
```
YYYY-MM-DD HH:MM:SS,MMM LEVEL logger_name: message
```
## Gateway State
```json
{
"pid": 12345,
"kind": "hermes-gateway",
"gateway_state": "running|startup_failed",
"exit_reason": "string|null",
"platforms": {},
"updated_at": "ISO8601"
}
```
## SQLite Contention Notes
Hermes uses WAL mode with aggressive retry (15 retries, 20-150ms jitter). Scarf must only open state.db in read-only mode to avoid write contention. Checkpoint every 50 writes. WAL file modification is a good signal for refresh.
+84
View File
@@ -0,0 +1,84 @@
# Internationalization (i18n)
Scarf uses Apple's modern **String Catalog** workflow. Source strings are auto-extracted from `Text("…")` and `String(localized: …)` literals into [`scarf/scarf/Localizable.xcstrings`](../scarf/Localizable.xcstrings) at build time (when built in Xcode.app; `xcodebuild` alone emits per-source `.stringsdata` but does not merge back into the catalog). Info.plist keys are localized via [`scarf/scarf/InfoPlist.xcstrings`](../scarf/InfoPlist.xcstrings).
## Languages
| Locale | Status |
|---|---|
| `en` (English) | Base / source |
| `zh-Hans` (Simplified Chinese) | AI-translated, native-speaker review welcome |
| `de` (German) | AI-translated, native-speaker review welcome |
| `fr` (French) | AI-translated, native-speaker review welcome |
| `es` (Spanish) | AI-translated, native-speaker review welcome |
| `ja` (Japanese) | AI-translated, native-speaker review welcome |
| `pt-BR` (Portuguese, Brazil) | AI-translated, native-speaker review welcome |
Canadian French users are served by base `fr`. `fr-CA` will be added only if a concrete Québec-specific bug is reported.
### Translation workflow
Source-of-truth per locale lives in `tools/translations/<locale>.json` — a flat `{ "English": "Translation" }` map. The merge step writes those into `scarf/scarf/Localizable.xcstrings` via:
```bash
python3 tools/merge-translations.py
```
Keys absent from a locale file fall back to English at runtime — this is deliberate for proper nouns (Scarf, Hermes, Anthropic, OAuth, SSH…) and format-only strings (`%lld`, `%@ → %@`, `•`). Re-running the merge is idempotent; iterate on a JSON and re-merge.
Contributor path for new languages is documented in the repo root [CONTRIBUTING.md](../../CONTRIBUTING.md#adding-a-language).
## Adding a new language
1. Xcode → Project → Info → Localizations → `+` (add locale).
2. Ensure the locale code is also listed in `knownRegions` of `scarf.xcodeproj/project.pbxproj`.
3. Open `Localizable.xcstrings` in Xcode; the new locale appears as an empty column — translate or use Xcode's AI suggestions.
4. Repeat for `InfoPlist.xcstrings` (microphone usage, etc.).
5. Smoke-test via scheme language override (Edit Scheme → Run → App Language).
## Adding translations (AI-first workflow)
For the three supported non-English locales we use Xcode's built-in AI translation:
1. Open `Localizable.xcstrings` in Xcode.
2. Select untranslated rows for a locale → right-click → **Translate** (Xcode 26+ provides GPT-backed suggestions with context from the surrounding code comment).
3. Review each suggestion before marking **Translated**.
4. For terms that should NOT translate (proper nouns like *Scarf*, *Hermes*, *Anthropic*; env var names; file paths), wrap the source site in `Text(verbatim: "…")` so the key never hits the catalog.
## Guardrails when writing new UI code
`Text("literal")` auto-localizes. These patterns **silently leak English** and need explicit handling:
| Pattern | Fix |
|---|---|
| `Text(someStringVar)` | `Text(LocalizedStringResource("key"))` or pass a `LocalizedStringKey` down the view tree |
| `"Hello " + name` | `String(localized: "Hello \(name)")` |
| `String(format: "$%.2f", cost)` | `cost.formatted(.currency(code: "USD").precision(.fractionLength(2)))` |
| `String(format: "%.1f MB", size)` | `Int64(size).formatted(.byteCount(style: .file))` |
| `String(format: "%.1fM", n)` | `n.formatted(.number.notation(.compactName))` |
| Custom `DateFormatter` with fixed `dateFormat` | `date.formatted(.dateTime.month().day().year())` |
| `.help(stringVar)` | Compute a `LocalizedStringKey` or use `.help(Text(…))` |
| `Button(stringVar)` | `Button(LocalizedStringResource("key")) { … }` |
Strings that are **user data** (session titles, memory file contents, log lines, shell commands shown in UI, file paths) should pass through without localization — this happens naturally when the value is a `String` variable, since those overloads skip the catalog.
## Audit status
Phase 1b (the `multi-language` PR) closed every tracked site from the original audit:
- **Category A high-priority (ternary UI copy)** — converted to `Text`-ternary form so each branch routes through `LocalizedStringKey`.
- **Category A medium-priority (enum `.rawValue` displays)** — each enum now exposes `displayName: LocalizedStringResource` and call sites use it. `LogEntry.LogLevel` (technical jargon) stays verbatim.
- **Category A lower-priority (displayName passthroughs)** — wrapped with `Text(verbatim:)` for proper nouns / user data (`HermesToolPlatform`, `ServerRegistry.Entry`, `MCPServerPreset`). `MCPTransport.displayName` promoted to `LocalizedStringResource`.
- **Category B (composite format strings)** — migrated to `Text("\(arg) suffix")` with `LocalizedStringKey` or to `.percent` / `.currency` FormatStyle.
- **Category C (hard-coded day names)** — replaced with `Calendar.current.shortWeekdaySymbols`, re-indexed to match the existing Mon=0 data model.
- **Category D (`.help(stringVar)` sites)** — `ConnectionStatusPill` now returns `Text` from its `labelText` / `tooltipText` properties.
If you spot a new silently-un-localizable site during translation review, prefer the patterns in the table above over one-off workarounds.
### Non-blocking (intentional verbatim)
The following are correct as-is because they pass user data or machine-readable content through to the UI:
- Session titles, message content, memory / skill / YAML file contents, log lines, shell commands, file paths, session IDs, model IDs, credential sources, URL strings.
If we later need to badge these (e.g. "(empty)" placeholder), the badge itself becomes a localizable key while the data passthrough stays verbatim.
+97
View File
@@ -0,0 +1,97 @@
# Scarf — Product Requirements Document
## Overview
Scarf is a native macOS application that provides a graphical interface for the Hermes AI agent. Hermes is a CLI-based AI agent with 40+ tools, multi-platform messaging, autonomous skill creation, persistent memory, and scheduled automation. Scarf gives users visibility into what Hermes is doing, when, and what it creates.
## Problem
Hermes operates entirely through CLI with no visual dashboard. Users cannot easily:
- See what the agent is currently doing or has done
- Browse conversation history across sessions
- Monitor tool executions in real-time
- Manage memory, skills, or cron jobs visually
- Chat with the agent through a native interface
## Target User
Developer running Hermes locally on macOS who wants transparency and control over agent activity.
## Core Features
### 1. Dashboard
- System health overview (model, provider, connection status)
- Active session indicator
- Token usage and cost summary (aggregated from session data)
- Gateway platform connection status
- Recent activity feed
### 2. Sessions Browser
- List all conversation sessions with metadata (source, message count, tool calls, cost, duration)
- Full conversation detail view with message rendering
- Full-text search across all sessions (via SQLite FTS5)
- Session lineage tracking (parent_session_id chains)
### 3. Activity Feed
- Real-time tool execution monitoring (the core transparency feature)
- Each entry: tool name, kind, arguments, result preview, timestamp
- Filterable by tool type, session, time range
- Color-coded by tool kind (read/edit/execute/fetch)
### 4. Live Chat
- Send messages to Hermes via ACP (Agent Client Protocol)
- Stream responses with tool calls shown inline
- Session management (new, load, resume)
### 5. Memory Viewer/Editor
- Display MEMORY.md and USER.md with markdown rendering
- Edit and save changes
- Character count vs configured limits
### 6. Skills Browser
- Tree view by category
- Skill metadata display
- Search and filter
### 7. Cron Manager
- View scheduled jobs with status, next/last run times
- View job output
- Enable/disable jobs
### 8. Log Viewer
- Real-time log tailing (errors.log, gateway.log)
- Level-based filtering and text search
### 9. Menu Bar Presence
- Status icon showing Hermes state (running/idle/error)
- Quick access to recent session, new chat
## Technical Constraints
- macOS 26.2+ (SwiftUI, Swift 6 concurrency)
- No external SPM dependencies — uses system SQLite3 C API, Foundation JSON
- Reads Hermes data from `~/.hermes/` (requires sandbox disabled)
- ACP communication via subprocess stdio JSON-RPC
- App sandbox disabled (developer tool needing filesystem access)
## Data Sources
| Source | Path | Format | Access |
|--------|------|--------|--------|
| Sessions DB | `~/.hermes/state.db` | SQLite (WAL) | Read-only |
| Session files | `~/.hermes/sessions/*.json` | JSON | Read-only |
| Config | `~/.hermes/config.yaml` | YAML | Read/Write |
| Memory | `~/.hermes/memories/*.md` | Markdown | Read/Write |
| Cron jobs | `~/.hermes/cron/jobs.json` | JSON | Read/Write |
| Cron output | `~/.hermes/cron/output/` | Text | Read-only |
| Logs | `~/.hermes/logs/*.log` | Text | Read-only |
| Gateway state | `~/.hermes/gateway_state.json` | JSON | Read-only |
| Skills | `~/.hermes/skills/` | Directory tree | Read-only |
| ACP | `hermes acp` subprocess | JSON-RPC stdio | Bidirectional |
## Non-Goals (v1)
- Config editing UI (read-only display for v1, except memory)
- Skill creation or management
- Gateway platform management
- Multi-user support
+58
View File
@@ -0,0 +1,58 @@
# Scarf — Feature Roadmap
## Tier 1 — High Value, Data Already Available
### 1. Insights Dashboard
Rich usage analytics pulled from the sessions and messages SQLite tables:
- Overview stats: sessions, messages, tool calls, tokens, active time, avg session duration
- Model breakdown: sessions and tokens per model
- Platform breakdown: CLI vs Telegram vs Discord usage
- Top tools chart: ranked tool usage with call counts and percentages
- Activity patterns: sessions by day-of-week, peak hours heatmap
- Notable sessions: longest, most messages, most tokens, most tool calls
- Time period selector: last 7/30/90 days
### 2. Tool Management Panel
- List all toolsets with enabled/disabled status and descriptions
- Toggle switches to enable/disable tools (via `hermes tools enable/disable`)
- Per-platform tool configuration
- MCP tool status
### 3. Session Management Enhancements
- Rename sessions from the Sessions browser (via `hermes sessions rename`)
- Delete sessions (via `hermes sessions delete`)
- Export sessions to JSONL (via `hermes sessions export`)
- Session stats card (total count, DB size, per-platform breakdown)
## Tier 2 — Medium Value, New Service Code Required
### 4. Skills Hub
- Search remote registries for new skills (6 sources)
- Install/uninstall skills from GUI
- Skill update indicator
- Trust level badges (builtin, local, hub)
### 5. Gateway Control Center
- Start/stop/restart gateway from GUI
- Real-time status: PID, uptime, connected platforms
- Pairing management: view approved users, approve/revoke
- Platform status per messaging service
### 6. System Health View
- Mirror `hermes status` and `hermes doctor` output
- API key validation, auth provider status, external tools
- Update available indicator
## Tier 3 — Nice to Have
### 7. Profile Management
- List/create/switch profiles (isolated Hermes instances)
### 8. Plugin Management
- Install from Git, enable/disable, update
### 9. MCP Server Management
- Add/remove/test MCP servers, toggle tools per server
### 10. Config Editor
- Structured form editor for config.yaml with validation
+658
View File
@@ -0,0 +1,658 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
53495AB62F7B992C00BD31AD /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 53SWIFTTERM0001 /* SwiftTerm */; };
53SPARKLE00010 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 53SPARKLE00011 /* Sparkle */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
534959502F7B83B700BD31AD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 534959382F7B83B600BD31AD /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5349593F2F7B83B600BD31AD;
remoteInfo = scarf;
};
5349595A2F7B83B700BD31AD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 534959382F7B83B600BD31AD /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5349593F2F7B83B600BD31AD;
remoteInfo = scarf;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
534959402F7B83B600BD31AD /* scarf.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = scarf.app; sourceTree = BUILT_PRODUCTS_DIR; };
5349594F2F7B83B700BD31AD /* scarfTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = scarfTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
534959592F7B83B700BD31AD /* scarfUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = scarfUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
534959AA2F7B83B600BD31AD /* Exceptions for "scarf" folder in "scarf" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 5349593F2F7B83B600BD31AD /* scarf */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
534959422F7B83B600BD31AD /* scarf */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
534959AA2F7B83B600BD31AD /* Exceptions for "scarf" folder in "scarf" target */,
);
path = scarf;
sourceTree = "<group>";
};
534959522F7B83B700BD31AD /* scarfTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = scarfTests;
sourceTree = "<group>";
};
5349595C2F7B83B700BD31AD /* scarfUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = scarfUITests;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
5349593D2F7B83B600BD31AD /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
53495AB62F7B992C00BD31AD /* SwiftTerm in Frameworks */,
53SPARKLE00010 /* Sparkle in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
5349594C2F7B83B700BD31AD /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
534959562F7B83B700BD31AD /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
534959372F7B83B600BD31AD = {
isa = PBXGroup;
children = (
534959422F7B83B600BD31AD /* scarf */,
534959522F7B83B700BD31AD /* scarfTests */,
5349595C2F7B83B700BD31AD /* scarfUITests */,
534959412F7B83B600BD31AD /* Products */,
);
sourceTree = "<group>";
};
534959412F7B83B600BD31AD /* Products */ = {
isa = PBXGroup;
children = (
534959402F7B83B600BD31AD /* scarf.app */,
5349594F2F7B83B700BD31AD /* scarfTests.xctest */,
534959592F7B83B700BD31AD /* scarfUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
5349593F2F7B83B600BD31AD /* scarf */ = {
isa = PBXNativeTarget;
buildConfigurationList = 534959632F7B83B700BD31AD /* Build configuration list for PBXNativeTarget "scarf" */;
buildPhases = (
5349593C2F7B83B600BD31AD /* Sources */,
5349593D2F7B83B600BD31AD /* Frameworks */,
5349593E2F7B83B600BD31AD /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
534959422F7B83B600BD31AD /* scarf */,
);
name = scarf;
packageProductDependencies = (
53SWIFTTERM0001 /* SwiftTerm */,
53SPARKLE00011 /* Sparkle */,
);
productName = scarf;
productReference = 534959402F7B83B600BD31AD /* scarf.app */;
productType = "com.apple.product-type.application";
};
5349594E2F7B83B700BD31AD /* scarfTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 534959662F7B83B700BD31AD /* Build configuration list for PBXNativeTarget "scarfTests" */;
buildPhases = (
5349594B2F7B83B700BD31AD /* Sources */,
5349594C2F7B83B700BD31AD /* Frameworks */,
5349594D2F7B83B700BD31AD /* Resources */,
);
buildRules = (
);
dependencies = (
534959512F7B83B700BD31AD /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
534959522F7B83B700BD31AD /* scarfTests */,
);
name = scarfTests;
packageProductDependencies = (
);
productName = scarfTests;
productReference = 5349594F2F7B83B700BD31AD /* scarfTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
534959582F7B83B700BD31AD /* scarfUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 534959692F7B83B700BD31AD /* Build configuration list for PBXNativeTarget "scarfUITests" */;
buildPhases = (
534959552F7B83B700BD31AD /* Sources */,
534959562F7B83B700BD31AD /* Frameworks */,
534959572F7B83B700BD31AD /* Resources */,
);
buildRules = (
);
dependencies = (
5349595B2F7B83B700BD31AD /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
5349595C2F7B83B700BD31AD /* scarfUITests */,
);
name = scarfUITests;
packageProductDependencies = (
);
productName = scarfUITests;
productReference = 534959592F7B83B700BD31AD /* scarfUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
534959382F7B83B600BD31AD /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2630;
LastUpgradeCheck = 2630;
TargetAttributes = {
5349593F2F7B83B600BD31AD = {
CreatedOnToolsVersion = 26.3;
};
5349594E2F7B83B700BD31AD = {
CreatedOnToolsVersion = 26.3;
TestTargetID = 5349593F2F7B83B600BD31AD;
};
534959582F7B83B700BD31AD = {
CreatedOnToolsVersion = 26.3;
TestTargetID = 5349593F2F7B83B600BD31AD;
};
};
};
buildConfigurationList = 5349593B2F7B83B600BD31AD /* Build configuration list for PBXProject "scarf" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
"zh-Hans",
de,
fr,
es,
ja,
"pt-BR",
);
mainGroup = 534959372F7B83B600BD31AD;
minimizedProjectReferenceProxies = 1;
packageReferences = (
53SWIFTTERM0002 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
53SPARKLE00012 /* XCRemoteSwiftPackageReference "Sparkle" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 534959412F7B83B600BD31AD /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
5349593F2F7B83B600BD31AD /* scarf */,
5349594E2F7B83B700BD31AD /* scarfTests */,
534959582F7B83B700BD31AD /* scarfUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
5349593E2F7B83B600BD31AD /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
5349594D2F7B83B700BD31AD /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
534959572F7B83B700BD31AD /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
5349593C2F7B83B600BD31AD /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
5349594B2F7B83B700BD31AD /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
534959552F7B83B700BD31AD /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
534959512F7B83B700BD31AD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5349593F2F7B83B600BD31AD /* scarf */;
targetProxy = 534959502F7B83B700BD31AD /* PBXContainerItemProxy */;
};
5349595B2F7B83B700BD31AD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5349593F2F7B83B600BD31AD /* scarf */;
targetProxy = 5349595A2F7B83B700BD31AD /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
534959612F7B83B700BD31AD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
534959622F7B83B700BD31AD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 3Q6X2L86C4;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
534959642F7B83B700BD31AD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 22;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = scarf/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
534959652F7B83B700BD31AD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 22;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = scarf/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
534959672F7B83B700BD31AD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/scarf.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/scarf";
};
name = Debug;
};
534959682F7B83B700BD31AD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/scarf.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/scarf";
};
name = Release;
};
5349596A2F7B83B700BD31AD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = scarf;
};
name = Debug;
};
5349596B2F7B83B700BD31AD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = scarf;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
5349593B2F7B83B600BD31AD /* Build configuration list for PBXProject "scarf" */ = {
isa = XCConfigurationList;
buildConfigurations = (
534959612F7B83B700BD31AD /* Debug */,
534959622F7B83B700BD31AD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
534959632F7B83B700BD31AD /* Build configuration list for PBXNativeTarget "scarf" */ = {
isa = XCConfigurationList;
buildConfigurations = (
534959642F7B83B700BD31AD /* Debug */,
534959652F7B83B700BD31AD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
534959662F7B83B700BD31AD /* Build configuration list for PBXNativeTarget "scarfTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
534959672F7B83B700BD31AD /* Debug */,
534959682F7B83B700BD31AD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
534959692F7B83B700BD31AD /* Build configuration list for PBXNativeTarget "scarfUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
5349596A2F7B83B700BD31AD /* Debug */,
5349596B2F7B83B700BD31AD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
53SPARKLE00012 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.6.0;
};
};
53SWIFTTERM0002 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
53SPARKLE00011 /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = 53SPARKLE00012 /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
};
53SWIFTTERM0001 /* SwiftTerm */ = {
isa = XCSwiftPackageProductDependency;
package = 53SWIFTTERM0002 /* XCRemoteSwiftPackageReference "SwiftTerm" */;
productName = SwiftTerm;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 534959382F7B83B600BD31AD /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5349593F2F7B83B600BD31AD"
BuildableName = "scarf.app"
BlueprintName = "scarf"
ReferencedContainer = "container:scarf.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5349594E2F7B83B700BD31AD"
BuildableName = "scarfTests.xctest"
BlueprintName = "scarfTests"
ReferencedContainer = "container:scarf.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "534959582F7B83B700BD31AD"
BuildableName = "scarfUITests.xctest"
BlueprintName = "scarfUITests"
ReferencedContainer = "container:scarf.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5349593F2F7B83B600BD31AD"
BuildableName = "scarf.app"
BlueprintName = "scarf"
ReferencedContainer = "container:scarf.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5349593F2F7B83B600BD31AD"
BuildableName = "scarf.app"
BlueprintName = "scarf"
ReferencedContainer = "container:scarf.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -0,0 +1,68 @@
{
"images" : [
{
"filename" : "AW Mac OS Applications-iOS-Default-16x16@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "AW Mac OS Applications-iOS-Default-16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "AW Mac OS Applications-iOS-Default-32x32@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "AW Mac OS Applications-iOS-Default-32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "AW Mac OS Applications-iOS-Default-128x128@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "AW Mac OS Applications-iOS-Default-128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "AW Mac OS Applications-iOS-Default-256x256@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "AW Mac OS Applications-iOS-Default-512x512@1x 1.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "AW Mac OS Applications-iOS-Default-512x512@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "AW Mac OS Applications-iOS-Default-1024x1024@1x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+80
View File
@@ -0,0 +1,80 @@
import SwiftUI
struct ContentView: View {
@Environment(AppCoordinator.self) private var coordinator
@Environment(\.serverContext) private var serverContext
/// Per-window connection status. Constructed from the window's
/// `serverContext` once; lifetime matches the window.
@State private var connectionStatus: ConnectionStatusViewModel
init() {
_connectionStatus = State(initialValue: ConnectionStatusViewModel(context: .local))
}
var body: some View {
NavigationSplitView {
SidebarView()
.navigationSplitViewColumnWidth(min: 180, ideal: 240, max: 360)
} detail: {
detailView
.toolbar {
ToolbarItem(placement: .navigation) {
ServerSwitcherToolbar()
}
if serverContext.isRemote {
// `.principal` centers the pill in the toolbar
// the native emphasis bezel is the intended frame;
// the pill's own visual content (icon + label, no
// background) sits inside it in balance.
ToolbarItem(placement: .principal) {
ConnectionStatusPill(status: connectionStatus)
}
}
}
.onAppear {
// The actual context is injected via @Environment, which
// isn't available in `init`. Rebuild the monitor here
// the first time we know the real context. Safe to call
// repeatedly; `startMonitoring()` cancels + restarts.
if connectionStatus.context.id != serverContext.id {
connectionStatus = ConnectionStatusViewModel(context: serverContext)
}
connectionStatus.startMonitoring()
}
.onDisappear { connectionStatus.stopMonitoring() }
}
}
@ViewBuilder
private var detailView: some View {
// Each routed view receives the window's `serverContext` in its
// init so its `@State` ViewModel is constructed bound to the right
// server. This is what makes multi-window work without it,
// every window's VMs default-construct with `.local` even though
// the surrounding env has the right context.
switch coordinator.selectedSection {
case .dashboard: DashboardView(context: serverContext)
case .insights: InsightsView(context: serverContext)
case .sessions: SessionsView(context: serverContext)
case .activity: ActivityView(context: serverContext)
case .projects: ProjectsView(context: serverContext)
case .chat: ChatView()
case .memory: MemoryView(context: serverContext)
case .skills: SkillsView(context: serverContext)
case .platforms: PlatformsView(context: serverContext)
case .personalities: PersonalitiesView(context: serverContext)
case .quickCommands: QuickCommandsView(context: serverContext)
case .credentialPools: CredentialPoolsView(context: serverContext)
case .plugins: PluginsView(context: serverContext)
case .webhooks: WebhooksView(context: serverContext)
case .profiles: ProfilesView(context: serverContext)
case .tools: ToolsView(context: serverContext)
case .mcpServers: MCPServersView(context: serverContext)
case .gateway: GatewayView(context: serverContext)
case .cron: CronView(context: serverContext)
case .health: HealthView(context: serverContext)
case .logs: LogsView(context: serverContext)
case .settings: SettingsView(context: serverContext)
}
}
}
+290
View File
@@ -0,0 +1,290 @@
import Foundation
// MARK: - JSON-RPC Transport
// Hand-written `encode(to:)` / `init(from:)` with explicit `nonisolated` so
// Swift 6's default-isolation doesn't synthesize a MainActor-isolated
// conformance which would prevent these payloads from being encoded or
// decoded inside `ACPClient`'s actor context (the JSON-RPC read/write loop).
// The member list must stay in sync with the stored properties above.
struct ACPRequest: Encodable, Sendable {
nonisolated let jsonrpc = "2.0"
nonisolated let id: Int
nonisolated let method: String
nonisolated let params: [String: AnyCodable]
enum CodingKeys: String, CodingKey { case jsonrpc, id, method, params }
nonisolated func encode(to encoder: any Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(jsonrpc, forKey: .jsonrpc)
try c.encode(id, forKey: .id)
try c.encode(method, forKey: .method)
try c.encode(params, forKey: .params)
}
}
struct ACPRawMessage: Decodable, Sendable {
nonisolated let jsonrpc: String?
nonisolated let id: Int?
nonisolated let method: String?
nonisolated let result: AnyCodable?
nonisolated let error: ACPError?
nonisolated let params: AnyCodable?
nonisolated var isResponse: Bool { id != nil && method == nil }
nonisolated var isNotification: Bool { method != nil && id == nil }
nonisolated var isRequest: Bool { method != nil && id != nil }
enum CodingKeys: String, CodingKey { case jsonrpc, id, method, result, error, params }
nonisolated init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.jsonrpc = try c.decodeIfPresent(String.self, forKey: .jsonrpc)
self.id = try c.decodeIfPresent(Int.self, forKey: .id)
self.method = try c.decodeIfPresent(String.self, forKey: .method)
self.result = try c.decodeIfPresent(AnyCodable.self, forKey: .result)
self.error = try c.decodeIfPresent(ACPError.self, forKey: .error)
self.params = try c.decodeIfPresent(AnyCodable.self, forKey: .params)
}
}
struct ACPError: Decodable, Sendable {
nonisolated let code: Int
nonisolated let message: String
enum CodingKeys: String, CodingKey { case code, message }
nonisolated init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.code = try c.decode(Int.self, forKey: .code)
self.message = try c.decode(String.self, forKey: .message)
}
}
// MARK: - AnyCodable (for dynamic JSON)
struct AnyCodable: Codable, @unchecked Sendable {
nonisolated let value: Any
nonisolated init(_ value: Any) { self.value = value }
// NOT marked `nonisolated`: Swift's default-isolation treats writes to a
// `let value: Any` stored property as MainActor-isolated even when the
// property is declared nonisolated (Any can't be strictly Sendable, so
// the compiler can't prove the write is safe off-main). Leaving the
// init as default-isolated silences the mutation warnings; the Decodable
// conformance is still usable from ACPClient's nonisolated read loop
// because all callers are already @preconcurrency with respect to
// `AnyCodable` (it's @unchecked Sendable).
init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
value = NSNull()
} else if let bool = try? container.decode(Bool.self) {
value = bool
} else if let int = try? container.decode(Int.self) {
value = int
} else if let double = try? container.decode(Double.self) {
value = double
} else if let string = try? container.decode(String.self) {
value = string
} else if let array = try? container.decode([AnyCodable].self) {
value = array.map(\.value)
} else if let dict = try? container.decode([String: AnyCodable].self) {
value = dict.mapValues(\.value)
} else {
value = NSNull()
}
}
func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
switch value {
case is NSNull:
try container.encodeNil()
case let bool as Bool:
try container.encode(bool)
case let int as Int:
try container.encode(int)
case let double as Double:
try container.encode(double)
case let string as String:
try container.encode(string)
case let array as [Any]:
try container.encode(array.map { AnyCodable($0) })
case let dict as [String: Any]:
try container.encode(dict.mapValues { AnyCodable($0) })
default:
try container.encodeNil()
}
}
// MARK: - Accessors
nonisolated var stringValue: String? { value as? String }
nonisolated var intValue: Int? { value as? Int }
nonisolated var dictValue: [String: Any]? { value as? [String: Any] }
nonisolated var arrayValue: [Any]? { value as? [Any] }
}
// MARK: - ACP Events (parsed from session/update notifications)
enum ACPEvent: Sendable {
case messageChunk(sessionId: String, text: String)
case thoughtChunk(sessionId: String, text: String)
case toolCallStart(sessionId: String, call: ACPToolCallEvent)
case toolCallUpdate(sessionId: String, update: ACPToolCallUpdateEvent)
case permissionRequest(sessionId: String, requestId: Int, request: ACPPermissionRequestEvent)
case promptComplete(sessionId: String, response: ACPPromptResult)
case availableCommands(sessionId: String, commands: [[String: Any]])
case connectionLost(reason: String)
case unknown(sessionId: String, type: String)
}
struct ACPToolCallEvent: Sendable {
let toolCallId: String
let title: String
let kind: String
let status: String
let content: String
let rawInput: [String: Any]?
var functionName: String {
// title format is "functionName: summary" or just "functionName"
let parts = title.split(separator: ":", maxSplits: 1)
return String(parts.first ?? Substring(title)).trimmingCharacters(in: .whitespaces)
}
var argumentsSummary: String {
let parts = title.split(separator: ":", maxSplits: 1)
if parts.count > 1 {
return String(parts[1]).trimmingCharacters(in: .whitespaces)
}
return ""
}
var argumentsJSON: String {
guard let input = rawInput,
let data = try? JSONSerialization.data(withJSONObject: input),
let str = String(data: data, encoding: .utf8) else { return "{}" }
return str
}
}
struct ACPToolCallUpdateEvent: Sendable {
let toolCallId: String
let kind: String
let status: String
let content: String
let rawOutput: String?
}
struct ACPPermissionRequestEvent: Sendable {
let toolCallTitle: String
let toolCallKind: String
let options: [(optionId: String, name: String)]
}
struct ACPPromptResult: Sendable {
let stopReason: String
let inputTokens: Int
let outputTokens: Int
let thoughtTokens: Int
let cachedReadTokens: Int
}
// MARK: - Event Parsing
enum ACPEventParser {
nonisolated static func parse(notification: ACPRawMessage) -> ACPEvent? {
guard notification.method == "session/update",
let params = notification.params?.dictValue,
let sessionId = params["sessionId"] as? String,
let update = params["update"] as? [String: Any],
let updateType = update["sessionUpdate"] as? String else {
return nil
}
switch updateType {
case "agent_message_chunk":
let text = extractContentText(from: update)
return .messageChunk(sessionId: sessionId, text: text)
case "agent_thought_chunk":
let text = extractContentText(from: update)
return .thoughtChunk(sessionId: sessionId, text: text)
case "tool_call":
let event = ACPToolCallEvent(
toolCallId: update["toolCallId"] as? String ?? "",
title: update["title"] as? String ?? "",
kind: update["kind"] as? String ?? "other",
status: update["status"] as? String ?? "pending",
content: extractContentArrayText(from: update),
rawInput: update["rawInput"] as? [String: Any]
)
return .toolCallStart(sessionId: sessionId, call: event)
case "tool_call_update":
let event = ACPToolCallUpdateEvent(
toolCallId: update["toolCallId"] as? String ?? "",
kind: update["kind"] as? String ?? "other",
status: update["status"] as? String ?? "completed",
content: extractContentArrayText(from: update),
rawOutput: update["rawOutput"] as? String
)
return .toolCallUpdate(sessionId: sessionId, update: event)
case "available_commands_update":
let commands = update["availableCommands"] as? [[String: Any]] ?? []
return .availableCommands(sessionId: sessionId, commands: commands)
default:
return .unknown(sessionId: sessionId, type: updateType)
}
}
nonisolated static func parsePermissionRequest(_ message: ACPRawMessage) -> ACPEvent? {
guard message.method == "session/request_permission",
let params = message.params?.dictValue,
let sessionId = params["sessionId"] as? String,
let requestId = message.id else { return nil }
let toolCall = params["toolCall"] as? [String: Any] ?? [:]
let optionsRaw = params["options"] as? [[String: Any]] ?? []
let options = optionsRaw.compactMap { opt -> (optionId: String, name: String)? in
guard let id = opt["optionId"] as? String,
let name = opt["name"] as? String else { return nil }
return (optionId: id, name: name)
}
let event = ACPPermissionRequestEvent(
toolCallTitle: toolCall["title"] as? String ?? "",
toolCallKind: toolCall["kind"] as? String ?? "other",
options: options
)
return .permissionRequest(sessionId: sessionId, requestId: requestId, request: event)
}
// MARK: - Content Extraction
nonisolated private static func extractContentText(from update: [String: Any]) -> String {
if let content = update["content"] as? [String: Any],
let text = content["text"] as? String {
return text
}
return ""
}
nonisolated private static func extractContentArrayText(from update: [String: Any]) -> String {
if let contentArray = update["content"] as? [[String: Any]] {
return contentArray.compactMap { item -> String? in
guard let inner = item["content"] as? [String: Any] else { return nil }
return inner["text"] as? String
}.joined(separator: "\n")
}
return ""
}
}
+486
View File
@@ -0,0 +1,486 @@
import Foundation
/// Settings for one of hermes's auxiliary model tasks (vision, compression, approvals, etc.).
/// Every auxiliary task follows the same provider/model/base_url/api_key/timeout pattern.
struct AuxiliaryModel: Sendable, Equatable {
var provider: String
var model: String
var baseURL: String
var apiKey: String
var timeout: Int
nonisolated static let empty = AuxiliaryModel(provider: "auto", model: "", baseURL: "", apiKey: "", timeout: 30)
}
/// Group of display-related settings mirroring the `display:` block in config.yaml.
struct DisplaySettings: Sendable, Equatable {
var skin: String
var compact: Bool
var resumeDisplay: String // "full" | "minimal"
var bellOnComplete: Bool
var inlineDiffs: Bool
var toolProgressCommand: Bool
var toolPreviewLength: Int
var busyInputMode: String // e.g. "interrupt"
nonisolated static let empty = DisplaySettings(
skin: "default",
compact: false,
resumeDisplay: "full",
bellOnComplete: false,
inlineDiffs: true,
toolProgressCommand: false,
toolPreviewLength: 0,
busyInputMode: "interrupt"
)
}
/// Container/terminal backend options. These map to `terminal.*` keys in config.yaml.
struct TerminalSettings: Sendable, Equatable {
var cwd: String
var timeout: Int
var envPassthrough: [String]
var persistentShell: Bool
var dockerImage: String
var dockerMountCwdToWorkspace: Bool
var dockerForwardEnv: [String]
var dockerVolumes: [String]
var containerCPU: Int // 0 = unlimited
var containerMemory: Int // MB, 0 = unlimited
var containerDisk: Int // MB, 0 = unlimited
var containerPersistent: Bool
var modalImage: String
var modalMode: String // "auto" | other
var daytonaImage: String
var singularityImage: String
nonisolated static let empty = TerminalSettings(
cwd: ".",
timeout: 180,
envPassthrough: [],
persistentShell: true,
dockerImage: "",
dockerMountCwdToWorkspace: false,
dockerForwardEnv: [],
dockerVolumes: [],
containerCPU: 0,
containerMemory: 0,
containerDisk: 0,
containerPersistent: false,
modalImage: "",
modalMode: "auto",
daytonaImage: "",
singularityImage: ""
)
}
/// Browser automation tuning (`browser.*`).
struct BrowserSettings: Sendable, Equatable {
var inactivityTimeout: Int
var commandTimeout: Int
var recordSessions: Bool
var allowPrivateURLs: Bool
var camofoxManagedPersistence: Bool
nonisolated static let empty = BrowserSettings(
inactivityTimeout: 120,
commandTimeout: 30,
recordSessions: false,
allowPrivateURLs: false,
camofoxManagedPersistence: false
)
}
/// Voice push-to-talk plus TTS/STT provider settings.
struct VoiceSettings: Sendable, Equatable {
var recordKey: String
var maxRecordingSeconds: Int
var silenceDuration: Double
// TTS
var ttsProvider: String
var ttsEdgeVoice: String
var ttsElevenLabsVoiceID: String
var ttsElevenLabsModelID: String
var ttsOpenAIModel: String
var ttsOpenAIVoice: String
var ttsNeuTTSModel: String
var ttsNeuTTSDevice: String
// STT
var sttEnabled: Bool
var sttProvider: String
var sttLocalModel: String
var sttLocalLanguage: String
var sttOpenAIModel: String
var sttMistralModel: String
nonisolated static let empty = VoiceSettings(
recordKey: "ctrl+b",
maxRecordingSeconds: 120,
silenceDuration: 3.0,
ttsProvider: "edge",
ttsEdgeVoice: "en-US-AriaNeural",
ttsElevenLabsVoiceID: "",
ttsElevenLabsModelID: "eleven_multilingual_v2",
ttsOpenAIModel: "gpt-4o-mini-tts",
ttsOpenAIVoice: "alloy",
ttsNeuTTSModel: "neuphonic/neutts-air-q4-gguf",
ttsNeuTTSDevice: "cpu",
sttEnabled: true,
sttProvider: "local",
sttLocalModel: "base",
sttLocalLanguage: "",
sttOpenAIModel: "whisper-1",
sttMistralModel: "voxtral-mini-latest"
)
}
/// Eight sub-models that share the same provider/model/base_url/api_key/timeout shape.
struct AuxiliarySettings: Sendable, Equatable {
var vision: AuxiliaryModel
var webExtract: AuxiliaryModel
var compression: AuxiliaryModel
var sessionSearch: AuxiliaryModel
var skillsHub: AuxiliaryModel
var approval: AuxiliaryModel
var mcp: AuxiliaryModel
var flushMemories: AuxiliaryModel
nonisolated static let empty = AuxiliarySettings(
vision: .empty,
webExtract: .empty,
compression: .empty,
sessionSearch: .empty,
skillsHub: .empty,
approval: .empty,
mcp: .empty,
flushMemories: .empty
)
}
/// Security/redaction/firewall config. Website blocklist is nested in YAML.
struct SecuritySettings: Sendable, Equatable {
var redactSecrets: Bool
var redactPII: Bool // from privacy.redact_pii
var tirithEnabled: Bool
var tirithPath: String
var tirithTimeout: Int
var tirithFailOpen: Bool
var blocklistEnabled: Bool
var blocklistDomains: [String]
nonisolated static let empty = SecuritySettings(
redactSecrets: true,
redactPII: false,
tirithEnabled: true,
tirithPath: "tirith",
tirithTimeout: 5,
tirithFailOpen: true,
blocklistEnabled: false,
blocklistDomains: []
)
}
/// Human-delay simulates realistic typing pace (`human_delay.*`).
struct HumanDelaySettings: Sendable, Equatable {
var mode: String // "off" | "natural" | "custom"
var minMS: Int
var maxMS: Int
nonisolated static let empty = HumanDelaySettings(mode: "off", minMS: 800, maxMS: 2500)
}
/// Compression / context routing.
struct CompressionSettings: Sendable, Equatable {
var enabled: Bool
var threshold: Double
var targetRatio: Double
var protectLastN: Int
nonisolated static let empty = CompressionSettings(enabled: true, threshold: 0.5, targetRatio: 0.2, protectLastN: 20)
}
struct CheckpointSettings: Sendable, Equatable {
var enabled: Bool
var maxSnapshots: Int
nonisolated static let empty = CheckpointSettings(enabled: true, maxSnapshots: 50)
}
struct LoggingSettings: Sendable, Equatable {
var level: String // DEBUG | INFO | WARNING | ERROR
var maxSizeMB: Int
var backupCount: Int
nonisolated static let empty = LoggingSettings(level: "INFO", maxSizeMB: 5, backupCount: 3)
}
struct DelegationSettings: Sendable, Equatable {
var model: String
var provider: String
var baseURL: String
var apiKey: String
var maxIterations: Int
nonisolated static let empty = DelegationSettings(model: "", provider: "", baseURL: "", apiKey: "", maxIterations: 50)
}
/// Discord-specific platform settings (`discord.*`). Other platforms currently have thinner schemas.
struct DiscordSettings: Sendable, Equatable {
var requireMention: Bool
var freeResponseChannels: String
var autoThread: Bool
var reactions: Bool
nonisolated static let empty = DiscordSettings(requireMention: true, freeResponseChannels: "", autoThread: true, reactions: true)
}
/// Telegram settings under `telegram.*` in config.yaml. Most Telegram tuning is
/// done via environment variables (`TELEGRAM_*`) this is the subset that lives
/// in the YAML.
struct TelegramSettings: Sendable, Equatable {
var requireMention: Bool
var reactions: Bool
nonisolated static let empty = TelegramSettings(requireMention: true, reactions: false)
}
/// Slack settings under `platforms.slack.*` (and a couple of top-level keys).
struct SlackSettings: Sendable, Equatable {
var replyToMode: String // "off" | "first" | "all"
var requireMention: Bool
var replyInThread: Bool
var replyBroadcast: Bool
nonisolated static let empty = SlackSettings(replyToMode: "first", requireMention: true, replyInThread: true, replyBroadcast: false)
}
/// Matrix settings under `matrix.*`.
struct MatrixSettings: Sendable, Equatable {
var requireMention: Bool
var autoThread: Bool
var dmMentionThreads: Bool
nonisolated static let empty = MatrixSettings(requireMention: true, autoThread: true, dmMentionThreads: false)
}
/// Mattermost settings. Mattermost is mostly driven by env vars; config.yaml
/// currently just exposes `group_sessions_per_user` at the top level, but we
/// reserve this struct for future expansion so the form has a stable type.
struct MattermostSettings: Sendable, Equatable {
var requireMention: Bool
var replyMode: String // "thread" | "off"
nonisolated static let empty = MattermostSettings(requireMention: true, replyMode: "off")
}
/// WhatsApp settings under `whatsapp.*`.
struct WhatsAppSettings: Sendable, Equatable {
var unauthorizedDMBehavior: String // "pair" | "ignore"
var replyPrefix: String
nonisolated static let empty = WhatsAppSettings(unauthorizedDMBehavior: "pair", replyPrefix: "")
}
/// Home Assistant filters under `platforms.homeassistant.extra`. Hermes ignores
/// every state change by default; users must opt-in via at least one filter.
struct HomeAssistantSettings: Sendable, Equatable {
var watchDomains: [String]
var watchEntities: [String]
var watchAll: Bool
var ignoreEntities: [String]
var cooldownSeconds: Int
nonisolated static let empty = HomeAssistantSettings(watchDomains: [], watchEntities: [], watchAll: false, ignoreEntities: [], cooldownSeconds: 30)
}
// MARK: - Root Config
struct HermesConfig: Sendable {
// Original fields preserved for zero breakage with existing call sites.
var model: String
var provider: String
var maxTurns: Int
var personality: String
var terminalBackend: String
var memoryEnabled: Bool
var memoryCharLimit: Int
var userCharLimit: Int
var nudgeInterval: Int
var streaming: Bool
var showReasoning: Bool
var verbose: Bool
var autoTTS: Bool
var silenceThreshold: Int
var reasoningEffort: String
var showCost: Bool
var approvalMode: String
var browserBackend: String
var memoryProvider: String
var dockerEnv: [String: String]
var commandAllowlist: [String]
var memoryProfile: String
var serviceTier: String
var gatewayNotifyInterval: Int
var forceIPv4: Bool
var contextEngine: String
var interimAssistantMessages: Bool
var honchoInitOnSessionStart: Bool
// Phase 1 additions
var timezone: String
var userProfileEnabled: Bool
var toolUseEnforcement: String // "auto" | "true" | "false" | comma list
var gatewayTimeout: Int
var approvalTimeout: Int
var fileReadMaxChars: Int
var cronWrapResponse: Bool
var prefillMessagesFile: String
var skillsExternalDirs: [String]
// Grouped blocks
var display: DisplaySettings
var terminal: TerminalSettings
var browser: BrowserSettings
var voice: VoiceSettings
var auxiliary: AuxiliarySettings
var security: SecuritySettings
var humanDelay: HumanDelaySettings
var compression: CompressionSettings
var checkpoints: CheckpointSettings
var logging: LoggingSettings
var delegation: DelegationSettings
var discord: DiscordSettings
var telegram: TelegramSettings
var slack: SlackSettings
var matrix: MatrixSettings
var mattermost: MattermostSettings
var whatsapp: WhatsAppSettings
var homeAssistant: HomeAssistantSettings
nonisolated static let empty = HermesConfig(
model: "unknown",
provider: "unknown",
maxTurns: 0,
personality: "default",
terminalBackend: "local",
memoryEnabled: false,
memoryCharLimit: 0,
userCharLimit: 0,
nudgeInterval: 0,
streaming: true,
showReasoning: false,
verbose: false,
autoTTS: true,
silenceThreshold: 200,
reasoningEffort: "medium",
showCost: false,
approvalMode: "manual",
browserBackend: "",
memoryProvider: "",
dockerEnv: [:],
commandAllowlist: [],
memoryProfile: "",
serviceTier: "normal",
gatewayNotifyInterval: 600,
forceIPv4: false,
contextEngine: "compressor",
interimAssistantMessages: true,
honchoInitOnSessionStart: false,
timezone: "",
userProfileEnabled: true,
toolUseEnforcement: "auto",
gatewayTimeout: 1800,
approvalTimeout: 60,
fileReadMaxChars: 100_000,
cronWrapResponse: true,
prefillMessagesFile: "",
skillsExternalDirs: [],
display: .empty,
terminal: .empty,
browser: .empty,
voice: .empty,
auxiliary: .empty,
security: .empty,
humanDelay: .empty,
compression: .empty,
checkpoints: .empty,
logging: .empty,
delegation: .empty,
discord: .empty,
telegram: .empty,
slack: .empty,
matrix: .empty,
mattermost: .empty,
whatsapp: .empty,
homeAssistant: .empty
)
}
// Hand-written `init(from:)` so Swift 6 doesn't synthesize a
// MainActor-isolated Decodable conformance (which would fail to be used from
// `HermesFileService.loadGatewayState()`, a nonisolated method).
struct GatewayState: Sendable, Codable {
nonisolated let pid: Int?
nonisolated let kind: String?
nonisolated let gatewayState: String?
nonisolated let exitReason: String?
nonisolated let platforms: [String: PlatformState]?
nonisolated let updatedAt: String?
enum CodingKeys: String, CodingKey {
case pid, kind
case gatewayState = "gateway_state"
case exitReason = "exit_reason"
case platforms
case updatedAt = "updated_at"
}
nonisolated init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.pid = try c.decodeIfPresent(Int.self, forKey: .pid)
self.kind = try c.decodeIfPresent(String.self, forKey: .kind)
self.gatewayState = try c.decodeIfPresent(String.self, forKey: .gatewayState)
self.exitReason = try c.decodeIfPresent(String.self, forKey: .exitReason)
self.platforms = try c.decodeIfPresent([String: PlatformState].self, forKey: .platforms)
self.updatedAt = try c.decodeIfPresent(String.self, forKey: .updatedAt)
}
nonisolated func encode(to encoder: any Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encodeIfPresent(pid, forKey: .pid)
try c.encodeIfPresent(kind, forKey: .kind)
try c.encodeIfPresent(gatewayState, forKey: .gatewayState)
try c.encodeIfPresent(exitReason, forKey: .exitReason)
try c.encodeIfPresent(platforms, forKey: .platforms)
try c.encodeIfPresent(updatedAt, forKey: .updatedAt)
}
nonisolated var isRunning: Bool {
gatewayState == "running"
}
nonisolated var statusText: String {
gatewayState ?? "unknown"
}
}
struct PlatformState: Sendable, Codable {
nonisolated let connected: Bool?
nonisolated let error: String?
enum CodingKeys: String, CodingKey { case connected, error }
nonisolated init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.connected = try c.decodeIfPresent(Bool.self, forKey: .connected)
self.error = try c.decodeIfPresent(String.self, forKey: .error)
}
nonisolated func encode(to encoder: any Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encodeIfPresent(connected, forKey: .connected)
try c.encodeIfPresent(error, forKey: .error)
}
}
@@ -0,0 +1,93 @@
import Foundation
import SQLite3
/// Deprecated module-level path statics. Preserved as thin forwarders to
/// `ServerContext.local.paths` so existing call sites continue to compile
/// while Phase 1 migrates them to a per-server `ServerContext`.
///
/// New code should accept a `ServerContext` and read `context.paths.<field>`.
enum HermesPaths: Sendable {
@available(*, deprecated, message: "use ServerContext.paths.home")
nonisolated static var home: String { ServerContext.local.paths.home }
@available(*, deprecated, message: "use ServerContext.paths.stateDB")
nonisolated static var stateDB: String { ServerContext.local.paths.stateDB }
@available(*, deprecated, message: "use ServerContext.paths.configYAML")
nonisolated static var configYAML: String { ServerContext.local.paths.configYAML }
@available(*, deprecated, message: "use ServerContext.paths.memoriesDir")
nonisolated static var memoriesDir: String { ServerContext.local.paths.memoriesDir }
@available(*, deprecated, message: "use ServerContext.paths.memoryMD")
nonisolated static var memoryMD: String { ServerContext.local.paths.memoryMD }
@available(*, deprecated, message: "use ServerContext.paths.userMD")
nonisolated static var userMD: String { ServerContext.local.paths.userMD }
@available(*, deprecated, message: "use ServerContext.paths.sessionsDir")
nonisolated static var sessionsDir: String { ServerContext.local.paths.sessionsDir }
@available(*, deprecated, message: "use ServerContext.paths.cronJobsJSON")
nonisolated static var cronJobsJSON: String { ServerContext.local.paths.cronJobsJSON }
@available(*, deprecated, message: "use ServerContext.paths.cronOutputDir")
nonisolated static var cronOutputDir: String { ServerContext.local.paths.cronOutputDir }
@available(*, deprecated, message: "use ServerContext.paths.gatewayStateJSON")
nonisolated static var gatewayStateJSON: String { ServerContext.local.paths.gatewayStateJSON }
@available(*, deprecated, message: "use ServerContext.paths.skillsDir")
nonisolated static var skillsDir: String { ServerContext.local.paths.skillsDir }
@available(*, deprecated, message: "use ServerContext.paths.errorsLog")
nonisolated static var errorsLog: String { ServerContext.local.paths.errorsLog }
@available(*, deprecated, message: "use ServerContext.paths.agentLog")
nonisolated static var agentLog: String { ServerContext.local.paths.agentLog }
@available(*, deprecated, message: "use ServerContext.paths.gatewayLog")
nonisolated static var gatewayLog: String { ServerContext.local.paths.gatewayLog }
@available(*, deprecated, message: "use ServerContext.paths.scarfDir")
nonisolated static var scarfDir: String { ServerContext.local.paths.scarfDir }
@available(*, deprecated, message: "use ServerContext.paths.projectsRegistry")
nonisolated static var projectsRegistry: String { ServerContext.local.paths.projectsRegistry }
@available(*, deprecated, message: "use ServerContext.paths.mcpTokensDir")
nonisolated static var mcpTokensDir: String { ServerContext.local.paths.mcpTokensDir }
@available(*, deprecated, message: "use HermesPathSet.hermesBinaryCandidates")
nonisolated static var hermesBinaryCandidates: [String] {
HermesPathSet.hermesBinaryCandidates
}
@available(*, deprecated, message: "use ServerContext.paths.hermesBinary")
nonisolated static var hermesBinary: String { ServerContext.local.paths.hermesBinary }
}
// MARK: - SQLite Constants
/// SQLITE_TRANSIENT tells SQLite to make its own copy of bound string data.
/// The C macro is defined as ((sqlite3_destructor_type)-1) which can't be imported directly into Swift.
nonisolated let sqliteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
// MARK: - Query Defaults
enum QueryDefaults: Sendable {
nonisolated static let sessionLimit = 100
nonisolated static let messageSearchLimit = 50
nonisolated static let toolCallLimit = 50
nonisolated static let sessionPreviewLimit = 10
nonisolated static let previewContentLength = 100
nonisolated static let logLineLimit = 200
nonisolated static let defaultSilenceThreshold = 200
}
// MARK: - File Size Formatting
enum FileSizeUnit: Sendable {
nonisolated static let kilobyte = 1_024.0
nonisolated static let megabyte = 1_048_576.0
}
+158
View File
@@ -0,0 +1,158 @@
import Foundation
struct HermesCronJob: Identifiable, Sendable, Codable {
nonisolated let id: String
nonisolated let name: String
nonisolated let prompt: String
nonisolated let skills: [String]?
nonisolated let model: String?
nonisolated let schedule: CronSchedule
nonisolated let enabled: Bool
nonisolated let state: String
nonisolated let deliver: String?
nonisolated let nextRunAt: String?
nonisolated let lastRunAt: String?
nonisolated let lastError: String?
nonisolated let preRunScript: String?
nonisolated let deliveryFailures: Int?
nonisolated let lastDeliveryError: String?
nonisolated let timeoutType: String?
nonisolated let timeoutSeconds: Int?
nonisolated let silent: Bool?
enum CodingKeys: String, CodingKey {
case id, name, prompt, skills, model, schedule, enabled, state, deliver, silent
case nextRunAt = "next_run_at"
case lastRunAt = "last_run_at"
case lastError = "last_error"
case preRunScript = "pre_run_script"
case deliveryFailures = "delivery_failures"
case lastDeliveryError = "last_delivery_error"
case timeoutType = "timeout_type"
case timeoutSeconds = "timeout_seconds"
}
nonisolated init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.id = try c.decode(String.self, forKey: .id)
self.name = try c.decode(String.self, forKey: .name)
self.prompt = try c.decode(String.self, forKey: .prompt)
self.skills = try c.decodeIfPresent([String].self, forKey: .skills)
self.model = try c.decodeIfPresent(String.self, forKey: .model)
self.schedule = try c.decode(CronSchedule.self, forKey: .schedule)
self.enabled = try c.decode(Bool.self, forKey: .enabled)
self.state = try c.decode(String.self, forKey: .state)
self.deliver = try c.decodeIfPresent(String.self, forKey: .deliver)
self.nextRunAt = try c.decodeIfPresent(String.self, forKey: .nextRunAt)
self.lastRunAt = try c.decodeIfPresent(String.self, forKey: .lastRunAt)
self.lastError = try c.decodeIfPresent(String.self, forKey: .lastError)
self.preRunScript = try c.decodeIfPresent(String.self, forKey: .preRunScript)
self.deliveryFailures = try c.decodeIfPresent(Int.self, forKey: .deliveryFailures)
self.lastDeliveryError = try c.decodeIfPresent(String.self, forKey: .lastDeliveryError)
self.timeoutType = try c.decodeIfPresent(String.self, forKey: .timeoutType)
self.timeoutSeconds = try c.decodeIfPresent(Int.self, forKey: .timeoutSeconds)
self.silent = try c.decodeIfPresent(Bool.self, forKey: .silent)
}
nonisolated func encode(to encoder: any Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(id, forKey: .id)
try c.encode(name, forKey: .name)
try c.encode(prompt, forKey: .prompt)
try c.encodeIfPresent(skills, forKey: .skills)
try c.encodeIfPresent(model, forKey: .model)
try c.encode(schedule, forKey: .schedule)
try c.encode(enabled, forKey: .enabled)
try c.encode(state, forKey: .state)
try c.encodeIfPresent(deliver, forKey: .deliver)
try c.encodeIfPresent(nextRunAt, forKey: .nextRunAt)
try c.encodeIfPresent(lastRunAt, forKey: .lastRunAt)
try c.encodeIfPresent(lastError, forKey: .lastError)
try c.encodeIfPresent(preRunScript, forKey: .preRunScript)
try c.encodeIfPresent(deliveryFailures, forKey: .deliveryFailures)
try c.encodeIfPresent(lastDeliveryError, forKey: .lastDeliveryError)
try c.encodeIfPresent(timeoutType, forKey: .timeoutType)
try c.encodeIfPresent(timeoutSeconds, forKey: .timeoutSeconds)
try c.encodeIfPresent(silent, forKey: .silent)
}
nonisolated var stateIcon: String {
switch state {
case "scheduled": return "clock"
case "running": return "play.circle"
case "completed": return "checkmark.circle"
case "failed": return "xmark.circle"
default: return "questionmark.circle"
}
}
nonisolated var deliveryDisplay: String? {
guard let deliver, !deliver.isEmpty else { return nil }
// v0.9.0 extends Discord routing to threads: `discord:<chat>:<thread>`.
if deliver.hasPrefix("discord:") {
let parts = deliver.dropFirst("discord:".count).split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
if parts.count == 2 {
return "Discord thread \(parts[1]) in \(parts[0])"
}
if parts.count == 1 {
return "Discord \(parts[0])"
}
}
return deliver
}
}
struct CronSchedule: Sendable, Codable {
nonisolated let kind: String
nonisolated let runAt: String?
nonisolated let display: String?
nonisolated let expression: String?
enum CodingKeys: String, CodingKey {
case kind
case runAt = "run_at"
case display
case expression
}
nonisolated init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.kind = try c.decode(String.self, forKey: .kind)
self.runAt = try c.decodeIfPresent(String.self, forKey: .runAt)
self.display = try c.decodeIfPresent(String.self, forKey: .display)
self.expression = try c.decodeIfPresent(String.self, forKey: .expression)
}
nonisolated func encode(to encoder: any Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(kind, forKey: .kind)
try c.encodeIfPresent(runAt, forKey: .runAt)
try c.encodeIfPresent(display, forKey: .display)
try c.encodeIfPresent(expression, forKey: .expression)
}
}
// Hand-written `init(from:)` / `encode(to:)` so Swift 6 doesn't synthesize a
// MainActor-isolated Codable conformance `HermesFileService.loadCronJobs`
// is nonisolated and needs to decode this from a background task.
struct CronJobsFile: Sendable, Codable {
nonisolated let jobs: [HermesCronJob]
nonisolated let updatedAt: String?
enum CodingKeys: String, CodingKey {
case jobs
case updatedAt = "updated_at"
}
nonisolated init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.jobs = try c.decode([HermesCronJob].self, forKey: .jobs)
self.updatedAt = try c.decodeIfPresent(String.self, forKey: .updatedAt)
}
nonisolated func encode(to encoder: any Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(jobs, forKey: .jobs)
try c.encodeIfPresent(updatedAt, forKey: .updatedAt)
}
}
@@ -0,0 +1,54 @@
import Foundation
enum MCPTransport: String, Sendable, Equatable, CaseIterable, Identifiable {
case stdio
case http
var id: String { rawValue }
var displayName: LocalizedStringResource {
switch self {
case .stdio: return "Local (stdio)"
case .http: return "Remote (HTTP)"
}
}
}
struct HermesMCPServer: Identifiable, Sendable, Equatable {
let name: String
let transport: MCPTransport
let command: String?
let args: [String]
let url: String?
let auth: String?
let env: [String: String]
let headers: [String: String]
let timeout: Int?
let connectTimeout: Int?
let enabled: Bool
let toolsInclude: [String]
let toolsExclude: [String]
let resourcesEnabled: Bool
let promptsEnabled: Bool
let hasOAuthToken: Bool
var id: String { name }
var summary: String {
switch transport {
case .stdio:
let argString = args.isEmpty ? "" : " " + args.joined(separator: " ")
return (command ?? "") + argString
case .http:
return url ?? ""
}
}
}
struct MCPTestResult: Sendable, Equatable {
let serverName: String
let succeeded: Bool
let output: String
let tools: [String]
let elapsed: TimeInterval
}
+134
View File
@@ -0,0 +1,134 @@
import Foundation
struct HermesMessage: Identifiable, Sendable {
let id: Int
let sessionId: String
let role: String
let content: String
let toolCallId: String?
let toolCalls: [HermesToolCall]
let toolName: String?
let timestamp: Date?
let tokenCount: Int?
let finishReason: String?
let reasoning: String?
var isUser: Bool { role == "user" }
var isAssistant: Bool { role == "assistant" }
var isToolResult: Bool { role == "tool" }
var hasReasoning: Bool { reasoning != nil && !(reasoning?.isEmpty ?? true) }
}
struct HermesToolCall: Identifiable, Sendable, Codable {
var id: String { callId }
let callId: String
let functionName: String
let arguments: String
enum CodingKeys: String, CodingKey {
case callId = "id"
case type
case function
}
enum FunctionKeys: String, CodingKey {
case name
case arguments
}
init(callId: String, functionName: String, arguments: String) {
self.callId = callId
self.functionName = functionName
self.arguments = arguments
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
callId = try container.decode(String.self, forKey: .callId)
let funcContainer = try container.nestedContainer(keyedBy: FunctionKeys.self, forKey: .function)
functionName = try funcContainer.decode(String.self, forKey: .name)
arguments = try funcContainer.decode(String.self, forKey: .arguments)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(callId, forKey: .callId)
try container.encode("function", forKey: .type)
var funcContainer = container.nestedContainer(keyedBy: FunctionKeys.self, forKey: .function)
try funcContainer.encode(functionName, forKey: .name)
try funcContainer.encode(arguments, forKey: .arguments)
}
var toolKind: ToolKind {
switch functionName {
case "read_file", "search_files", "vision_analyze": return .read
case "write_file", "patch": return .edit
case "terminal", "execute_code": return .execute
case "web_search", "web_extract": return .fetch
case "browser_navigate", "browser_click", "browser_screenshot": return .browser
default: return .other
}
}
var argumentsSummary: String {
guard let data = arguments.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return arguments
}
if let command = json["command"] as? String {
return command
}
if let path = json["path"] as? String {
return path
}
if let query = json["query"] as? String {
return query
}
if let url = json["url"] as? String {
return url
}
return arguments.prefix(120) + (arguments.count > 120 ? "..." : "")
}
}
enum ToolKind: String, Sendable, CaseIterable {
case read
case edit
case execute
case fetch
case browser
case other
var displayName: LocalizedStringResource {
switch self {
case .read: return "Read"
case .edit: return "Edit"
case .execute: return "Execute"
case .fetch: return "Fetch"
case .browser: return "Browser"
case .other: return "Other"
}
}
var icon: String {
switch self {
case .read: return "doc.text.magnifyingglass"
case .edit: return "pencil"
case .execute: return "terminal"
case .fetch: return "globe"
case .browser: return "safari"
case .other: return "gearshape"
}
}
var color: String {
switch self {
case .read: return "green"
case .edit: return "blue"
case .execute: return "orange"
case .fetch: return "purple"
case .browser: return "indigo"
case .other: return "gray"
}
}
}
@@ -0,0 +1,92 @@
import Foundation
/// The filesystem layout of a Hermes installation, parameterized by the
/// `home` directory. The same layout is used for local installations (where
/// `home` is an absolute macOS path like `/Users/alan/.hermes`) and for
/// remote installations reached over SSH (where `home` is a remote path like
/// `/home/deploy/.hermes` or an unexpanded `~/.hermes` that the remote shell
/// will resolve).
///
/// Every path that used to live as a module-level static on `HermesPaths` is
/// an instance property here. `ServerContext.paths` is the canonical way to
/// reach these values; the old `HermesPaths` statics are preserved as
/// deprecated forwarders so Phase 1 can migrate call sites incrementally.
struct HermesPathSet: Sendable, Hashable {
let home: String
/// `true` when this path set belongs to a remote installation. Affects
/// only `hermesBinary` resolution every other path is identical in
/// shape between local and remote.
let isRemote: Bool
/// Pre-resolved remote binary path (e.g. `/home/deploy/.local/bin/hermes`).
/// Populated by `SSHTransport` once `command -v hermes` has run on the
/// target host. Unused when `isRemote == false`.
let binaryHint: String?
// MARK: - Defaults
/// Absolute path to the local user's `~/.hermes` directory.
nonisolated static let defaultLocalHome: String = {
let user = ProcessInfo.processInfo.environment["HOME"] ?? NSHomeDirectory()
return user + "/.hermes"
}()
/// Default remote home when the user doesn't override it in `SSHConfig`.
/// We leave `~` unexpanded on purpose the remote shell resolves it.
nonisolated static let defaultRemoteHome: String = "~/.hermes"
// MARK: - Paths (mirror of the old HermesPaths layout)
nonisolated var stateDB: String { home + "/state.db" }
nonisolated var configYAML: String { home + "/config.yaml" }
nonisolated var envFile: String { home + "/.env" }
nonisolated var authJSON: String { home + "/auth.json" }
nonisolated var soulMD: String { home + "/SOUL.md" }
nonisolated var pluginsDir: String { home + "/plugins" }
nonisolated var memoriesDir: String { home + "/memories" }
nonisolated var memoryMD: String { memoriesDir + "/MEMORY.md" }
nonisolated var userMD: String { memoriesDir + "/USER.md" }
nonisolated var sessionsDir: String { home + "/sessions" }
nonisolated var cronJobsJSON: String { home + "/cron/jobs.json" }
nonisolated var cronOutputDir: String { home + "/cron/output" }
nonisolated var gatewayStateJSON: String { home + "/gateway_state.json" }
nonisolated var skillsDir: String { home + "/skills" }
nonisolated var errorsLog: String { home + "/logs/errors.log" }
nonisolated var agentLog: String { home + "/logs/agent.log" }
nonisolated var gatewayLog: String { home + "/logs/gateway.log" }
nonisolated var scarfDir: String { home + "/scarf" }
nonisolated var projectsRegistry: String { scarfDir + "/projects.json" }
nonisolated var mcpTokensDir: String { home + "/mcp-tokens" }
// MARK: - Binary resolution
/// Install locations we probe for the local `hermes` binary, in priority
/// order. Checked on every access so a user installing via a different
/// method doesn't need to relaunch Scarf.
nonisolated static let hermesBinaryCandidates: [String] = {
let user = ProcessInfo.processInfo.environment["HOME"] ?? NSHomeDirectory()
return [
user + "/.local/bin/hermes", // pipx / pip --user (default)
"/opt/homebrew/bin/hermes", // Homebrew on Apple Silicon
"/usr/local/bin/hermes", // Homebrew on Intel / manual install
user + "/.hermes/bin/hermes" // Some self-install layouts
]
}()
/// Resolved path to the `hermes` executable for this installation.
///
/// Local: returns the first executable candidate, falling back to the
/// pipx default so error messages still make sense on a fresh machine.
///
/// Remote: returns `binaryHint` (populated at connect time) or bare
/// `"hermes"` as a last-resort default that relies on the remote `$PATH`.
nonisolated var hermesBinary: String {
if isRemote {
return binaryHint ?? "hermes"
}
for path in Self.hermesBinaryCandidates
where FileManager.default.isExecutableFile(atPath: path) {
return path
}
return Self.hermesBinaryCandidates[0]
}
}
@@ -0,0 +1,59 @@
import Foundation
struct HermesSession: Identifiable, Sendable {
let id: String
let source: String
let userId: String?
let model: String?
let title: String?
let parentSessionId: String?
let startedAt: Date?
let endedAt: Date?
let endReason: String?
let messageCount: Int
let toolCallCount: Int
let inputTokens: Int
let outputTokens: Int
let cacheReadTokens: Int
let cacheWriteTokens: Int
let estimatedCostUSD: Double?
let reasoningTokens: Int
let actualCostUSD: Double?
let costStatus: String?
let billingProvider: String?
var isSubagent: Bool { parentSessionId != nil }
var totalTokens: Int { inputTokens + outputTokens + reasoningTokens }
var displayCostUSD: Double? { actualCostUSD ?? estimatedCostUSD }
var costIsActual: Bool { actualCostUSD != nil }
var duration: TimeInterval? {
guard let start = startedAt, let end = endedAt else { return nil }
return end.timeIntervalSince(start)
}
var displayTitle: String {
title ?? id
}
var sourceIcon: String {
KnownPlatforms.icon(for: source)
}
func withTitle(_ newTitle: String) -> HermesSession {
HermesSession(
id: id, source: source, userId: userId, model: model,
title: newTitle, parentSessionId: parentSessionId,
startedAt: startedAt, endedAt: endedAt, endReason: endReason,
messageCount: messageCount, toolCallCount: toolCallCount,
inputTokens: inputTokens, outputTokens: outputTokens,
cacheReadTokens: cacheReadTokens, cacheWriteTokens: cacheWriteTokens,
estimatedCostUSD: estimatedCostUSD, reasoningTokens: reasoningTokens,
actualCostUSD: actualCostUSD, costStatus: costStatus,
billingProvider: billingProvider
)
}
}
+16
View File
@@ -0,0 +1,16 @@
import Foundation
struct HermesSkillCategory: Identifiable, Sendable {
let id: String
let name: String
let skills: [HermesSkill]
}
struct HermesSkill: Identifiable, Sendable {
let id: String
let name: String
let category: String
let path: String
let files: [String]
let requiredConfig: [String]
}
@@ -0,0 +1,17 @@
import Foundation
/// A slash command available in chat. Sourced either from the ACP server
/// (`available_commands_update`) or from user-defined `quick_commands` in
/// `config.yaml`.
struct HermesSlashCommand: Identifiable, Sendable, Equatable {
enum Source: Sendable, Equatable {
case acp
case quickCommand
}
var id: String { name }
let name: String
let description: String
let argumentHint: String?
let source: Source
}
+54
View File
@@ -0,0 +1,54 @@
import Foundation
struct HermesToolset: Identifiable, Sendable {
var id: String { name }
let name: String
let description: String
let icon: String
var enabled: Bool
}
struct HermesToolPlatform: Identifiable, Sendable {
var id: String { name }
let name: String
let displayName: String
let icon: String
}
enum KnownPlatforms {
static let cli = HermesToolPlatform(name: "cli", displayName: "CLI", icon: "terminal")
static let all: [HermesToolPlatform] = [
cli,
HermesToolPlatform(name: "telegram", displayName: "Telegram", icon: "paperplane"),
HermesToolPlatform(name: "discord", displayName: "Discord", icon: "bubble.left.and.bubble.right"),
HermesToolPlatform(name: "slack", displayName: "Slack", icon: "number"),
HermesToolPlatform(name: "whatsapp", displayName: "WhatsApp", icon: "phone.bubble"),
HermesToolPlatform(name: "signal", displayName: "Signal", icon: "lock.shield"),
HermesToolPlatform(name: "email", displayName: "Email", icon: "envelope"),
HermesToolPlatform(name: "homeassistant", displayName: "Home Assistant", icon: "house"),
HermesToolPlatform(name: "webhook", displayName: "Webhook", icon: "arrow.up.right.square"),
HermesToolPlatform(name: "matrix", displayName: "Matrix", icon: "lock.rectangle.stack"),
HermesToolPlatform(name: "feishu", displayName: "Feishu", icon: "message.badge.circle"),
HermesToolPlatform(name: "mattermost", displayName: "Mattermost", icon: "bubble.left.and.exclamationmark.bubble.right"),
HermesToolPlatform(name: "imessage", displayName: "iMessage", icon: "message.fill"),
]
static func icon(for platform: String) -> String {
switch platform {
case "cli": return "terminal"
case "telegram": return "paperplane"
case "discord": return "bubble.left.and.bubble.right"
case "slack": return "number"
case "whatsapp": return "phone.bubble"
case "signal": return "lock.shield"
case "email": return "envelope"
case "homeassistant": return "house"
case "webhook": return "arrow.up.right.square"
case "matrix": return "lock.rectangle.stack"
case "feishu": return "message.badge.circle"
case "mattermost": return "bubble.left.and.exclamationmark.bubble.right"
case "imessage": return "message.fill"
default: return "bubble.left"
}
}
}

Some files were not shown because too many files have changed in this diff Show More