Claude 892d778b08 feat(termux): F-Droid deep-link + RUN_COMMAND_PENDING_INTENT install path
Make the on-device Termux install flow actually work end-to-end. Three
workstreams from plans/atomic-wondering-sunrise.md plus a validation
patch:

Workstream A — F-Droid deep-link from WelcomeScreen
- The 'Install locally (Termux)' card no longer renders a dead disabled
  Continue button when Termux is missing. It now shows two stacked CTAs:
  primary 'Open F-Droid' (existing termux_install_action string, now
  actually used) deep-linking https://f-droid.org/packages/com.termux/,
  and secondary 'Or get it from GitHub Releases' (new string) for
  side-loaders deep-linking github.com/termux/termux-app/releases.
- Reuses existing HermesApi.openExternal; no new IPC method needed.

Workstream B — HermesInstaller backend-aware shell dispatch
- runShell / runHermesDoctor / runPipInstall in HermesInstaller now
  branch on currentBackend() and dispatch to TermuxRunner on Termux,
  bundled.runPython on Bundled. Pre-fix, every stage shelled through
  bundled.runPython regardless of backend, so Termux installs failed
  immediately at stage 3 with 'Bundled Python not available'.

Workstream C — RUN_COMMAND_PENDING_INTENT for real exit codes
- New TermuxResultReceiver.kt: BroadcastReceiver + TermuxResultRegistry
  process-singleton. Atomic session IDs, ConcurrentHashMap of
  Continuations, PendingIntent.FLAG_MUTABLE result intent builder
  (FLAG_MUTABLE is required — Termux's RunCommandService adds the
  result bundle to the PendingIntent before broadcasting it back).
- New TermuxRunner.runAndWait(command, cwd): suspend fn using
  suspendCancellableCoroutine (modeled on GatewayClient.request),
  attaches the result PendingIntent via the
  com.termux.RUN_COMMAND_PENDING_INTENT intent extra. Resolves with a
  BundledPythonRunner.PythonResult carrying the real exit code +
  truncated stdout/stderr (~100KB cap imposed by Termux).
- HermesInstaller.runShellViaTermux switches from termux.run to
  termux.runAndWait. The 4 call sites (stages 3/4/5/8 in runStages,
  plus runHermesUpdate) already gate on exitCode != 0 and consume
  stderr/stdout, so they become correct automatically.
- applyPatches rewritten as suspend: dispatches git apply via Termux
  using a bash heredoc to write the patch content to Termux's
  $PREFIX/tmp/ (our app's process can't write into Termux's sandbox).
  Drops the now-stale TODO(workstream-B-followup) comment.
- AndroidManifest gets a <queries> block declaring Termux package
  visibility — without it, on API 30+, PackageManager.getPackageInfo
  returns NameNotFoundException even when Termux is installed and
  HermesInstaller falls through to the bundled-Python backend.
  Also adds com.termux.permission.RUN_COMMAND (user-granted in
  Android Settings -> App info -> Additional permissions).
- TermuxProbe.isRunCommandResultSupported() helper checks Termux
  versionName >= 0.109 (the floor for RUN_COMMAND_PENDING_INTENT).
- InstallScreen surfaces a TermuxPermissionNeededCard when an install
  stage fails with a Termux-specific error pattern (RUN_COMMAND,
  allow-external-apps, plugin_action_disabled — NOT the bare word
  'permission' which pip prints routinely). The card has an 'Open
  App Settings' button using a new HermesApi.openAppSettings()
  helper that fires Settings.ACTION_APPLICATION_DETAILS_SETTINGS.
- setup-android.sh gains a queries-merge step. Anchored on
  start-of-line for awk and probes android:name="com.termux" for
  detect-and-skip (so a future library's unrelated <queries> block
  doesn't silently skip the Termux merge).

Validation patch — markerDir + Termux sandbox awareness (B1–B5)
- An adversarial review found that HermesInstaller assumed every path
  it computes (hermesPython, hermesRepo, hermesVenv, the verified
  marker) lives somewhere our app can read. On Termux backend
  hermesHome resolves to /data/data/com.termux/files/home/.hermes
  which is OFF-LIMITS to our process. Pre-Workstream-C the install
  never succeeded on Termux so these checks were never exercised
  against real state; Workstream C made the install actually work,
  so the broken file-existence checks now blocked every user from
  ever reaching AppState.Main.
- Fix: backend-aware markerDir at context.filesDir/hermes-markers/
  on Termux (hermesHome on Bundled). New sentinels
  hermesVerifiedMarker / hermesRepoClonedMarker /
  hermesVenvCreatedMarker live there. Stages 3 & 4 write their
  marker on success and check the marker (not the file) for
  skip-on-rerun. checkInstall().installed/configured/hasApiKey
  collapse to 'verified marker exists' on Termux (the marker IS
  the success signal of a completed install).
- Stage 4 switches from python3.11 to python on Termux — Termux's
  pkg install python ships the binary as 'python' symlinked to the
  current 3.x; the literal 'python3.11' was a Bundled-path
  assumption that happened to work on some Termux setups but not
  others.
- getHermesVersion becomes suspend + backend-aware (Termux dispatches
  the importlib.metadata.version call via runAndWait); HermesApi
  cascades.

Plan + research artifacts
- plans/atomic-wondering-sunrise.md is the live source-of-truth
  plan/postmortem (3 shipped workstreams + validation section).
- docs/python-bundling-alternatives.md captures the research that
  showed Termux + RESULT_INTENT is the right v1 path and that the
  originally-planned Chaquopy workstream isn't needed. Alpine
  minirootfs + proot is the recommended Tier 2 alternative for
  users without Termux, downloaded on first run (no APK bloat, no
  GPL contagion, F-Droid compatible).

Build status
- ./gradlew :app:assembleDebug green (arm64-v8a, armeabi-v7a, x86_64,
  universal APKs, ~20MB each).
- script/check-kotlin-drift.sh green (59 Kotlin files in sync).
- graphify graph rebuilt (1681 nodes, 2866 edges).

Untested on device. Verification steps for happy path,
permission-denied regression, and cancellation hygiene are in the
plan's Workstream C verification section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:54:41 -04:00

hermes-mobile

An Android port of hermes-desktop that runs hermes-agent on the device. The UI is a native Jetpack Compose port — no WebView, no Capacitor, no vendored React renderer. The Hermes Kotlin sources live in android-runner/; the live apps/mobile/android/ tree is regenerated by apps/mobile/scripts/setup-android.sh on every build. See docs/architecture.md for the full layout and design rationale.

The parity roadmap is at ~/.claude/plans/groovy-fluttering-island.md (8 phases, ~16 weeks, full desktop parity). The Phase 0 cleanup is complete; Phase 1 (HermesApi surface expansion) is the next milestone.

Layout

hermes-mobile/
├── android-runner/                          # canonical Kotlin sources
│   ├── README.md                            # edit conventions
│   └── app/src/main/
│       ├── kotlin/com/nousresearch/hermes/  # 33 .kt files: HermesApi, MainActivity,
│       │                                    #   GatewayClient, HermesInstaller, services,
│       │                                    #   Compose UI (chat, sessions, memory, ...)
│       ├── AndroidManifest.xml
│       └── res/values/strings.xml
├── apps/mobile/
│   ├── android/                             # generated Android project; Kotlin sources
│   │                                        #   synced from android-runner/ on every build
│   ├── package.json                         # minimal stub with android:assemble:* scripts
│   └── scripts/
│       └── setup-android.sh                 # rsync --delete runner → live; merge strings
│                                            #   and permissions
├── script/
│   ├── check-kotlin-drift.sh                # CI: fail if android-runner/ and live/ diverge
│   ├── sign-and-zipalign.sh
│   ├── write-update-manifest.sh
│   └── audit-a11y.sh
├── f-droid/                                 # F-Droid metadata + build script
├── keystore/                                # release.jks (RSA 4096, 25-year)
├── review/
│   ├── hermes-desktop/                      # upstream fathah/hermes-desktop (visual spec)
│   └── hermes-agent/                        # upstream NousResearch/hermes-agent (HTTP API)
├── docs/
│   └── architecture.md
└── .github/workflows/mobile-build.yml        # CI: sync, drift check, build, sign, release

Build

# One-time setup
pnpm install
bash apps/mobile/scripts/setup-android.sh   # rsync runner → live
bash script/check-kotlin-drift.sh          # verify in sync

# Build
cd apps/mobile/android
./gradlew :app:assembleDebug               # debug APK
./gradlew :app:assembleRelease             # release APK (needs keystore)
./gradlew test                             # unit tests
./gradlew connectedAndroidTest             # instrumented tests

Editing

Edit Kotlin sources in android-runner/, not in apps/mobile/android/. The live tree is regenerated by setup-android.sh on every build. CI's script/check-kotlin-drift.sh fails the build on any divergence.

See android-runner/README.md for the layout, edit conventions, and "how to add a new Hermes source" walkthrough.

Distribution

Channel Build Update mechanism
Sideload (GitHub Releases) assembleRelease (signed) In-app updater reads latest.json from the release
F-Droid f-droid/build.sh F-Droid client auto-update
Manual APK assembleDebug None

Parity status

Desktop Mobile (this repo)
Screens 20+ 3 implemented (chat, sessions, memory); 13+ TODO
IPC methods ~188 44 implemented; ~110 TODO (Phase 1)
Connection modes Local + remote + SSH All three implemented
Install Curl install.sh Termux + bundled Python (8 stages)
Gateway Python subprocess Python subprocess (same)
OAuth All providers OAuth loop broken; fix in Phase 1
Updates electron-updater GitHub Releases (F-Droid: in-app disabled)
Crash reporting Optional Sentry Optional Sentry (TODO Phase 7)

See ~/.claude/plans/groovy-fluttering-island.md for the full roadmap.

S
Description
No description provided
Readme 8.1 MiB
Languages
Kotlin 97.5%
Shell 2.4%
Java 0.1%