27 Commits
Author SHA1 Message Date
Bailey Dixon faf4281486 fix(pairing): omit inactive Tailscale API routes 2026-08-30 20:15:42 -04:00
Bailey Dixon 8ee772f5fb fix(pairing): prefer dedicated Tailscale listener 2026-08-30 19:56:05 -04:00
Bailey Dixon ce63030bcf fix(pairing): reject ambiguous dashboard ingress 2026-08-30 18:15:36 -04:00
Bailey Dixon 92f99a4120 fix(pairing): bind relay to selected dashboard origin 2026-08-30 18:02:53 -04:00
Bailey Dixon 85ba13bd04 fix(pairing): prefer recommended Tailscale listener 2026-08-30 15:44:56 -04:00
Bailey Dixon 841e237905 fix(pairing): reject public plaintext routes 2026-08-30 15:11:56 -04:00
Bailey Dixon 01533337eb fix(pairing): route new invites through dashboard ingress 2026-08-30 15:02:55 -04:00
Bailey Dixon 318a02db49 fix(pairing): preserve relay routes from QR 2026-08-29 13:58:29 -04:00
Bailey Dixon 47b471acb4 feat(relay): add dashboard same-origin ingress 2026-08-26 21:54:56 -04:00
Bailey Dixon b0a7cf0494 feat: add self-hosted secure connection routes 2026-08-13 15:31:46 -04:00
Bailey Dixon cdeccd69e4 feat: add secure relay route selection 2026-08-12 20:10:41 -04:00
Bailey DixonandClaude Fable 5 865c39bd86 fix(plugin): package-relative imports so the native plugin loader works (#165)
hermes-agent's native installer imports the plugin directory as
hermes_plugins.hermes_relay — no top-level 'plugin' package exists there,
so every absolute 'from plugin.X' import crashed 'hermes relay start'
with ModuleNotFoundError: No module named 'plugin'.

- Convert all runtime absolute plugin.* imports to package-relative form
  (relay voice/realtime chain, tailscale CLI, pair, enhancements, tools).
- android_tool's direct-script fallback now imports the sibling module
  bare instead of via 'plugin.tools.'.
- dashboard/plugin_api.py is exec'd standalone by the dashboard web
  server (spec_from_file_location, no parent package), so relative
  imports can't work there: add a _plugin_module() bootstrap that
  imports through the real parent package when one exists, and
  otherwise synthesizes it (bare ModuleType with __path__ at the plugin
  dir under a stable sys.modules alias) without exec'ing
  plugin/__init__.py side effects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 18:49:07 -04:00
Bailey Dixon c090169545 feat(plugin): bundle relay management surface
Align the relay plugin/server metadata to 1.1.0 and add a version-track checker for Android, server/plugin, and desktop release surfaces.
2026-06-16 09:36:07 -04:00
Bailey Dixon d2c169f2fd feat(desktop): add tray pairing and consent flow 2026-05-17 21:53:01 -04:00
Bailey Dixon 5f0a52afbf fix: normalize Tailscale pairing endpoints 2026-05-04 21:29:03 -04:00
Bailey Dixon 3f0ef8101e fix: harden Android Tailscale pairing routes 2026-05-04 20:37:00 -04:00
Bailey Dixon c7f71903a7 feat: improve relay auth and Android connection handling 2026-05-04 19:19:22 -04:00
Bailey DixonandClaude Opus 4.7 f8b97763bc feat(multi-endpoint): smoothness pass — roles in subtitle, chip in chat bar, funnel auto-detect, loose probes, proxy-consent gate, re-pair hint
Six unrelated polish items that collectively make the ADR 24
multi-endpoint flow legible and hard to misconfigure:

1. Connections list subtitle shows role *names* not count.
   Previously "2 endpoints" — accurate but opaque. Now
   "Active: LAN • LAN + Public" so the user sees *which* roles the
   QR carries at a glance, matching the Settings → Connection Card
   1.5 info density.

2. Re-pair hint on single-endpoint connections.
   When the active card has exactly one endpoint stored (legacy
   single-URL pair), an inline tertiary-container strip suggests
   re-pairing with mode=auto. Inline Re-pair button wired to the
   existing onRepair callback.

3. Active-endpoint chip in the Chat top bar.
   Compact tappable chip next to the ambient-mode button surfacing
   the currently-resolved role (LAN/Tailscale/Public/Custom VPN).
   Tap jumps to Connections so the user can probe/override without
   leaving chat. Hidden when no endpoint is resolved (single-
   endpoint legacy pairings).

4. Loose resolver probe timing (2s → 4s, 30s → 60s cache).
   ADR 24 speced 2s probe + 30s cache. LTE handoff and slow hotel
   Wi-Fi routinely tripped the 2s false-negative. NetworkCallback
   still invalidates the cache on real network changes so the
   longer cache is functionally equivalent but burns less battery.

5. PairDialog proxy-fronted consent gate.
   The Advanced API-server override warning was informational only —
   the dialog still auto-minted a QR the phone would fail to use.
   Now when the pinned host trips the proxy heuristic the auto-mint
   pauses and the dialog shows "Mint anyway / Clear override"
   inline. Consent is per-host: changing host resets
   proxyConfirmed so a new FQDN triggers a fresh confirm.

6. Tailscale Funnel auto-detect for the public candidate.
   plugin/relay/tailscale.py adds funnel_url(port) that probes
   ``tailscale serve status --json`` for AllowFunnel flags and
   returns ``https://<hostname>/`` when the relay port is
   funneled. plugin/pair.py build_endpoint_candidates calls it as
   a fallback when mode=auto|public is picked without an explicit
   --public-url. Removes the "pin public URL on Remote Access tab"
   step when Funnel is already publishing. Soft-fail on every
   error path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:14:26 -04:00
Bailey DixonandClaude Opus 4.7 e914810ea3 feat(pairing): --prefer priority override on all pair surfaces (ADR 24)
Adds explicit "promote this role to priority 0" control so operators
can force a specific endpoint path (Tailscale, public, custom VPN)
during testing or when the natural LAN → Tailscale → Public order
isn't what they want.

Surfaces:
- CLI: `hermes-pair --mode auto --prefer tailscale`
- Skill: documented in skills/devops/hermes-relay-pair/SKILL.md
- Dashboard Remote Access tab: "Prefer role:" dropdown on the
  Endpoint Preview card; consumed on "Regenerate QR".

Semantics:
- Open-vocab role string (not a closed enum) — any role emitted by
  build_endpoint_candidates can be named. Matching is
  case-insensitive + whitespace-trimmed; stored verbatim.
- Promoted role becomes priority 0, others shifted down one.
- Unknown role → stderr warning + natural order (fail-soft so
  operators see what's actually in the candidate list).
- Already-priority-0 role → no-op.

Tests: 6 new BuildEndpointCandidatesPreferTests covering all
semantics. Full suite 77 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:50:52 -04:00
Bailey DixonandClaude Opus 4.7 fae8ccd58f feat(pairing): multi-endpoint pairing + first-class Tailscale (ADR 24, 25)
Multi-endpoint pairing (ADR 24):
- QR schema now carries an optional ordered `endpoints` array with
  strict priority + reachability-as-tiebreaker semantics. Emits
  `hermes:3` when endpoints are present; `hermes:2` stays when
  only a legacy single-URL pair is emitted. Old v1/v2 QRs parse
  unchanged — phone synthesizes a priority-0 candidate for them.
- Reachability probe: HEAD /health per candidate, 2s timeout, 30s
  per-endpoint cache. NetworkCallback re-probes on network change.
- Paired Devices screen now renders one row per (device, endpoint);
  Settings gains an Endpoints card with per-endpoint health chip +
  manual override + "probe now" + SPKI pin inspection.
- Open-vocabulary roles: `lan` / `tailscale` / `public` get styled
  chips; unknown roles render as "Custom VPN (<role>)". HMAC
  canonicalization preserves role strings verbatim + array order.

First-class Tailscale (ADR 25):
- New `plugin/relay/tailscale.py` + `tailscale_cli.py` helpers +
  `scripts/hermes-relay-tailscale` shell shim (mirrors `hermes-pair`).
  Shells `tailscale serve --bg --https=<port> http://127.0.0.1:<port>`.
- `install.sh` gains optional step 7/7 — silent skip when binary
  absent or TS_DECLINE=1; auto-enable with TS_AUTO=1.
- `pair.py --mode auto` auto-detects Tailscale and emits a
  priority-1 `tailscale` endpoint when available.
- Auto-retires when upstream hermes-agent PR #9295 merges, via
  `canonical_upstream_present()` probe — same shape as
  `hermes_relay_bootstrap/`.

Dashboard Remote Access tab:
- New 5th tab surfacing Tailscale status, public URL pin, live
  reachability, and one-click QR regen with current modes.
- 6 new loopback-only proxy routes under
  `/api/plugins/hermes-relay/remote-access/*`.
- Bundle rebuilt (plugin/dashboard/dist/index.js).

Docs:
- New `docs/remote-access.md` operator-facing setup guide covering
  Tailscale (recommended) / Caddy+LE / Cloudflare Tunnel /
  WireGuard / plaintext-over-VPN with working config blocks.
- security.md + relay-server.md + spec.md §3.3 + README.md +
  CHANGELOG.md + DEVLOG.md + CLAUDE.md all synced.

Tests:
- Python: 71 new/updated tests (pairing schema + QR sign canonical
  form + Tailscale helper + dashboard proxy routes).
- Android: 9 HermesPairingPayload tests (v1/v2/v3 + role vocab) +
  7 MockWebServer-backed EndpointResolver tests.
- Lint + compileGooglePlayDebugKotlin green locally.

Restored `profilesUpdatedEvents` emitter in AuthManager /
ConnectionViewModel that the Kt-Payload agent had collaterally
deleted while adding endpoint persistence — unrelated but the
bundle would not compile without it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:37:31 -04:00
Bailey DixonandClaude Opus 4.6 d32c70138d feat(pair): finish wiring manual pairing fallback + per-channel revoke UI
Closes the loop on the half-wired in-app fallback pairing flow that's been
sitting since Phase 3 landed, and adds per-channel grant revoke to the
Paired Devices screen so users can yank one channel without nuking the
whole session.

## Manual pair fallback — host side (plugin/pair.py)

New `--register-code <CODE>` flag composes with the existing TTL / grants /
transport-hint flags. Skips the QR rendering pipeline entirely and just
pre-registers the supplied code with the local relay over the same loopback
`/pairing/register` endpoint the QR flow already uses.

- `normalize_pairing_code()` — 6-char A-Z/0-9 validator with clear errors
- `register_code_command()` — probes relay, pre-registers, prints "tap
  Connect" instructions
- `pair_command()` short-circuits to the new path when --register-code is set

Composes with all existing flags:
  hermes-pair --register-code ABCD12 --ttl 30d --grants chat:never,bridge:7d

Exit codes: 0 success, 1 relay unreachable / rejection, 2 arg validation.

Tests: plugin/tests/test_register_code.py — 25 stdlib unittest cases across
3 classes (validator branches, command happy + error paths, wire-shape
assertions via patched urlopen). All pass in 0.004s.

## Manual pair fallback — phone side UX polish

ConnectionSettingsScreen Card 3 (was the bare "code + copy + regenerate"
stub) is now a numbered three-step walkthrough:
  1. Copy the code (with copy + regenerate icon buttons inline)
  2. Run `hermes-pair --register-code <CODE>` (rendered in a tappable
     monospace span — one-tap copies the full command)
  3. Real "Connect" button that fires applyServerIssuedCodeAndReset →
     disconnectRelay → connectRelay, with in-flight spinner state +
     LocalSnackbarHost result feedback via classifyError("pair", ...)

Below the steps: an expandable "How does this work?" explainer covering
when this is the right flow (no camera / SSH-only / single-device pair),
how the code mechanism works end-to-end, and the reminder that bridge
control is gated by the master toggle on the Bridge tab — not by this
code. New private ManualPairStep composable for the numbered step badges.

## Per-channel grant revoke — Paired Devices screen

Each device card's GrantChip now has an inline x icon. Tapping it opens
an AlertDialog confirming "Revoke <channel> access for <device>?" with a
reminder that the session itself stays paired and other channels keep
their existing expiry.

New ConnectionViewModel.revokeChannelGrant(tokenPrefix, channel) which:
- Reads cached PairedDeviceInfo grants
- Rebuilds the full grants map converting absolute-epoch back to seconds-
  from-now (clamped to >= 1 because the relay-side _materialize_grants
  interprets 0 as "never expire")
- Replaces the target channel with 1L (~instantly expired by the time
  the PATCH lands)
- Calls relayHttpClient.extendSession(tokenPrefix, ttlSeconds=null,
  grants=rebuilt)
- Snackbar + device list refresh on success

GrantChip rewritten to show relative TTL ("never" / "in 6d" / "in 23h" /
"expired") instead of absolute short date. Used a small clickable Box
instead of IconButton for the chip x because IconButton's 48dp minimum
inflates the FlowRow visually.

The full-session "Revoke" button is unchanged — per-channel chips are
additive, not replacements.

## Docs

- skills/devops/hermes-relay-pair/SKILL.md — Manual fallback section
  between Procedure and Pitfalls with workflow + composition rules
- skills/devops/hermes-relay-self-setup/SKILL.md — "If you can't scan
  a QR" subsection in section D
- user-docs/reference/configuration.md — expanded Manual pairing code
  bullet into a 3-step walkthrough
- user-docs/guide/getting-started.md — camera-unavailable tip callout
- README.md — one-line bullet under the install section
- DEVLOG.md — full entries for both halves under 2026-04-12

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:09:24 -04:00
Bailey Dixon 309f132d61 fix(pair): drop QR error-correction to low for smaller terminal render
The signed payload is long enough (~500-800 bytes with HMAC signature)
that error="m" was producing a high QR version number and a visibly
oversized terminal render. Dropping to error="l" saves 2-3 QR versions
(~16-24 fewer modules across), which is the single biggest lever left
after compact=True + border=1. Applied to both terminal and PNG paths
so they stay consistent.

Scannability is unaffected on a phone camera at normal distances —
error="l" still has 7% redundancy, well above the noise floor for a
direct-scan use case where the QR is rendered cleanly on screen.
2026-04-11 22:18:54 -04:00
Bailey Dixon f3f8fd2fc2 fix(pair): render terminal QR compactly with border=1
Shrinks the hermes-pair / /hermes-relay-pair QR so it fits a normal
terminal window. Was already using compact=True (half-blocks); adds
border=1 to drop the default 4-module quiet zone. Content, signing,
and scan reliability unchanged.
2026-04-11 21:55:09 -04:00
Bailey DixonandClaude Opus 4.6 11bf512eaa feat(auth): pairing + security architecture — grants, TTL picker, Keystore, TOFU, Paired Devices
Full overhaul of the pairing model + session security based on Bailey's
design request. Everything lives in files we own; no upstream patches.

## Why

The existing model was minimal: one-shot pairing codes → fixed-30-day
session tokens → no channel separation → EncryptedSharedPreferences
storage. After the inbound media work exposed the "phone rate-limited
to death on relay restart" gap, the entire pairing story was ready
for a pass. Bailey asked for: user-chosen TTL at pair time (including
"never"), per-channel grants, secure-by-default transport UX, device
revocation UI, Android Keystore storage, TOFU cert pinning, QR signing,
Phase 3 bidirectional pairing foundation, and Tailscale detection.

Never expire is explicitly always selectable per Bailey's direction:
dont force check, just allow based on user intent. User agency over
policy.

## Server - plugin/relay/

- auth.py: Session gains grants + transport_hint + first_seen; math.inf
  for never-expire (serializes as null); PairingMetadata dataclass
  threaded through PairingManager.register_code + consume_code (returns
  metadata or None, not bool); SessionManager create_session accepts
  ttl_seconds + grants + transport_hint; _materialize_grants clamps
  per-channel grants to session lifetime; list_sessions + find_by_prefix
  + revoke for the new routes; RateLimiter.clear_all_blocks() called
  unconditionally when /pairing/register succeeds (fixes the stale
  block prevents re-pair bug). Default caps: terminal 30d, bridge 7d.

- server.py: /pairing/register accepts ttl_seconds/grants/transport_hint
  metadata (host wins over phone); new /pairing/approve (Phase 3 stub);
  new GET /sessions (tokens masked to 8-char prefix, is_current flag);
  new DELETE /sessions/{prefix} (200/404/409, self-revoke flagged);
  _authenticate threads pairing metadata into new session;
  _detect_transport_hint sniffs SSL from request transport; auth.ok
  payload carries expires_at + grants + transport_hint (inf to null).

- qr_sign.py NEW: HMAC-SHA256 over canonicalized JSON. Canonical form
  strips sig so signing is idempotent. load_or_create_secret reads or
  creates ~/.hermes/hermes-relay-qr-secret (32 bytes, 0o600).
  allow_nan=False so signing math.inf crashes loudly.

- pair.py + cli.py: new --ttl and --grants flags. parse_duration for
  1d/7d/30d/90d/1y/never + loose patterns. parse_grants for
  terminal=7d,bridge=1d. build_payload(sign=True) auto-bumps hermes 1
  to 2 when v2 fields present and attaches HMAC signature.
  read_relay_config detects TLS via RELAY_SSL_CERT. render_text_block
  shows Pair for 30 days / indefinitely plus grant labels.

- __version__ stays at 0.2.0 per Bailey: we never released 0.2.0.

- Tests: 4 new test modules covering qr_sign, session_grants,
  sessions_routes, rate_limit_clear. Existing media tests still pass.

## Phone - app/src/main/kotlin/.../

- auth/SessionTokenStore.kt NEW: interface + KeystoreTokenStore
  (StrongBox-preferred) + LegacyEncryptedPrefsTokenStore fallback.
  tryCreate swallows exceptions. One-shot lossless migration.
  hasHardwareBackedStorage flag surfaced in UI.

- auth/CertPinStore.kt NEW: TOFU cert pinning. SHA-256 SPKI fingerprints
  per host:port in DataStore. recordPinIfAbsent on first successful wss
  handshake; buildPinnerSnapshot produces an OkHttp CertificatePinner.
  removePinFor called by applyServerIssuedCodeAndReset on QR re-pair.

- auth/PairedSession.kt NEW: PairedSession state class + PairedDeviceInfo
  wire model.

- auth/AuthManager.kt: lazy-init SessionTokenStore picker with
  migration; currentPairedSession StateFlow; parses expires_at/grants/
  transport_hint from auth.ok; authenticate(ttlSeconds) injects
  ttl_seconds + grants into pairing-mode auth envelope;
  applyServerIssuedCodeAndReset wipes TOFU pin for target host.

- data/PairingPreferences.kt NEW: DataStore keys pair_ttl_seconds,
  insecure_ack_seen, insecure_reason, tofu_pins.

- util/TailscaleDetector.kt NEW: NetworkInterface scan for tailscale0
  plus 100.64.0.0/10 CGNAT plus relay URL host check. Informational
  only.

- ui/components/SessionTtlPickerDialog.kt NEW: radio list
  1d/7d/30d/90d/1y/Never. Defaults: QR wins, wss/Tailscale 30d,
  ws 7d, unknown 30d. Never warning inline, NOT gated.

- ui/components/TransportSecurityBadge.kt NEW: three states, three
  sizes.

- ui/components/InsecureConnectionAckDialog.kt NEW: first-time consent
  with reason picker. Reason persists for display, not gating.

- ui/screens/PairedDevicesScreen.kt NEW: full list with transport
  badge + expiry + grant chips + revoke button. Self-revoke wipes
  local state + redirects to pair.

- network/RelayHttpClient.kt: new listSessions and
  revokeSession(tokenPrefix) methods. 404 handled as endpoint not yet
  implemented for forward-compat.

- network/ConnectionManager.kt: optional CertPinStore param. Rebuilds
  OkHttpClient on every connect with fresh CertificatePinner snapshot.

- ui/components/QrPairingScanner.kt: HermesPairingPayload gains sig
  field and defaults; RelayPairing gains ttlSeconds/grants/
  transportHint. parseHermesPairingQr no longer rejects on version
  mismatch.

- viewmodel/ConnectionViewModel.kt: wires AuthManager before
  ConnectionManager so pin store is available. Exposes
  currentPairedSession, pairedDevices, isTailscaleDetected,
  insecureAckSeen, insecureReason.

- ui/RelayApp.kt: new Screen.PairedDevices route.

- ui/screens/SettingsScreen.kt: TransportSecurityBadge + Tailscale
  chip + hardware storage chip + Paired Devices row in Connection
  card. Insecure toggle routes through ack dialog. QR scan handoff
  opens SessionTtlPickerDialog.

- ui/components/ConnectionInfoSheet.kt: SessionInfoSheet shows
  Expires / Channel grants / Transport / Key storage rows.

## Docs

DEVLOG entry, ADR 15, spec.md section 3.3/3.3.1/3.4 rewrites, relay
route tables in docs + user-docs, Paired Devices subsection in
configuration.md, pair flow extension in getting-started.md, CLAUDE.md
Current State + Key Files + Integration Points.

## Known gaps filed for follow-up

- Phone-side QR signature verification (parsing works; full verify
  needs secret distribution path)
- Bidirectional pairing full UX tied to Phase 3 bridge
- Per-device role model (admin vs user)
- Grant renewal UI on Paired Devices
- Build verification: no gradle run this session; Bailey deploys from
  Android Studio

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:06:04 -04:00
Bailey DixonandClaude Opus 4.6 6fc366b6af feat(pairing): QR carries relay credentials — one scan, both channels
Reworks the relay pairing flow so it matches the API-side flow we already
had: the operator runs `hermes pair` on the host (the trust anchor), the
relay pre-registers a fresh pairing code via a new loopback-only endpoint,
and the QR payload carries both API credentials AND relay credentials.
The phone scans once and is fully configured for chat + terminal.

## Wire format

HermesPairingPayload now has an optional nested `relay` object:

    {
      "hermes": 1,
      "host": "<api-ip>", "port": 8642, "key": "<bearer>", "tls": false,
      "relay": { "url": "ws://<ip>:8767", "code": "ABCD12" }
    }

Old API-only QRs still parse cleanly (kotlinx.serialization
`ignoreUnknownKeys = true`, nullable field). The `relay` block is only
present when `hermes pair` found a running relay and pre-registration
succeeded.

## Server side

- `POST /pairing/register` on the relay: accepts `{"code":"…"}`, calls
  `PairingManager.register_code()`, returns `{"ok":true}` on success.
  Gated to `127.0.0.1` / `::1` — a remote LAN attacker cannot inject
  pairing codes; only host-shell processes can.
- `plugin/relay/config.py` — `PAIRING_ALPHABET` broadened from the
  no-ambiguous-chars 32-char set to the full 36-char `A-Z0-9` set so it
  matches `AuthManager.PAIRING_CODE_CHARS` on the app side. The earlier
  restriction silently rejected valid codes containing `0/O/1/I`.

## `hermes pair` changes

- Probes `http://127.0.0.1:<relay_port>/health` on startup.
- If the relay is up: mints a 6-char code, POSTs `/pairing/register`,
  embeds `relay.{url,code}` in the QR payload.
- If not: emits an `[info]` line pointing at `hermes relay start` and
  renders an API-only QR (backward compat).
- New `--no-relay` flag to force API-only even when a relay is running.
- Text block now has a "Relay (terminal + bridge)" section alongside
  "Server" so manual entry is possible when QR scanning fails.
- Warning is now gated to "any credential present" rather than just
  "API key present" — both types trigger the "don't share screenshots"
  notice.

## App side

- `HermesPairingPayload` gains optional `RelayPairing(url, code)`.
- `AuthManager.applyServerIssuedCode(code)` stores a one-shot override
  that trumps the locally-generated code on the next `authenticate()`
  call. Cleared automatically on `auth.ok` so subsequent reconnects use
  the long-lived session token. Local generation stays as the fallback
  and as the Phase-3 bridge direction trust anchor (phone issues code,
  host approves).
- `OnboardingScreen` — `onComplete` signature grew a `relayPairingCode`
  param; `ConnectPage` gets an `onRelayPairingDetected` callback that
  propagates scanned relay fields up to the parent. Applied to
  AuthManager in `RelayApp.kt` before nav transition.
- `SettingsScreen` — scanner handler auto-configures the relay URL,
  flips insecure mode on for `ws://` URLs, and calls
  `applyServerIssuedCode()`. Toast message distinguishes "API + relay
  configured" from "API only".
- `ConnectionManager.connect()` — URL normalizer appends `/ws` if the
  path is empty, so a bare `ws://host:port` still upgrades cleanly.
  Pairs with the server-side `/` → `/ws` alias added earlier.

## Docs

README, docs/{spec,decisions,relay-server}.md,
user-docs/{guide/getting-started,reference/relay-server}.md, and DEVLOG
all updated to describe the new single-scan flow + `/pairing/register`
endpoint + `--no-relay` flag + alphabet change.

Verified locally:
  gradlew :app:assembleDebug — BUILD SUCCESSFUL
  python -m py_compile plugin/relay/server.py plugin/pair.py
  python -m plugin.pair --help  / python -m plugin.relay --help
  adb install -r app-debug.apk — Success

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:31:41 -04:00
Bailey DixonandClaude Opus 4.6 b379d02d4c chore: prepare v0.1.0 Play Store release
- Rename "Hermes Relay" -> "Hermes-Relay" across docs, app strings,
  scripts, workflows, plugin, and relay server (~57 files)
- Redesign user-docs landing: install section moved above features
  via home-hero-after slot (InstallSection.vue), hero image replaced
  with phone-framed demo video that crossfades to brand logo after
  12s of playback (HeroDemo.vue). Fix dead /guide/getting-started
  links in raw HTML hrefs that VitePress base-rewriting skipped.
- Add chat demo to README, docs landing hero, and Getting Started's
  Verify Connection section. Re-encode 20MB / 102fps source capture
  to 1.95MB / 30fps with extracted poster frame for instant LCP.
- Add smooth auto-scroll for streaming chat with Settings toggle --
  follows new tokens at the bottom, pauses when user scrolls up.
- Migrate SettingsScreen clipboard from deprecated LocalClipboardManager
  to LocalClipboard suspend API (Compose 1.7+).
- Migrate scripts/screenshots from monolithic .bat to thin wrapper
  + screenshots.py for Play Store screenshot capture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:15:07 -04:00
Bailey DixonandClaude Opus 4.6 c2dc336045 feat: migrate hermes-pair to plugin CLI and add one-line installer
Replaces the standalone bash skill with a plugin-owned CLI sub-command
and adds a curl|bash installer so users can go from zero to connected
without cloning the repo.

Plugin migration:
- plugin/pair.py — pure-Python pairing logic (config reading, LAN IP
  detection, QR rendering, text fallback). Cross-platform socket-based
  LAN detection replaces Linux-only 'ip route'. Config fallback chain:
  config.yaml → ~/.hermes/.env → env vars → defaults
- plugin/cli.py — registers 'hermes pair' via v0.8.0 register_cli API
  with --png, --no-qr, --host, --port flags
- plugin/__init__.py — calls ctx.register_cli_command() wrapped in
  try/except so the 14 android_* tools still register on v0.7.0
- Pure-Python QR rendering via segno — no qrencode binary dependency
- Text block always shown alongside QR so pairing works inside Hermes
  Rich TUI and over limited SSH sessions where QR blocks garble
- Plugin version bumped to 0.3.0; segno>=1.6.0 added to requirements,
  setup.py, pyproject.toml

One-line installer:
- install.sh at repo root: curl|bash entry point that clones the plugin,
  installs Python deps, and prints next steps. Supports HERMES_HOME and
  HERMES_RELAY_BRANCH env overrides. Uses trap cleanup for temp dirs.
  Replaces the old plugin/install.sh (its URL comment already pointed
  to the root location)

Docs restructure:
- README 'Install' → 'Quick Start' with the one-liner front and center
- Homepage (user-docs/index.md) gets an 'Install in 30 seconds' block
  below the feature cards with scoped CSS
- Guide landing page leads with Quick Install
- Getting Started restructured into 3-step Quick Start flow
- CLAUDE.md key files table updated with plugin/cli.py and plugin/pair.py

Deprecation:
- skills/hermes-pairing-qr/SKILL.md gets deprecation frontmatter +
  notice in body pointing to 'hermes pair'
- skills/hermes-pairing-qr/hermes-pair prints a deprecation warning
  to stderr but stays functional for v0.7.0 users

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:18:01 -04:00