{"owner":"kurikomi-labs","repo":"komi-store","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","CLAUDE.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to WARP (warp.dev) when working with code in this repository.\n\n## Project Overview\n\nKomi Store is a cross-platform app store for GitHub releases built with **Kotlin Multiplatform (KMP)** and **Compose Multiplatform**. It targets **Android** (min API 26, target 36) and **Desktop** (Windows, macOS, Linux via JVM).\n\nPackage: `zed.rainxch.githubstore`\n\n## Build & Run Commands\n\n```bash\n# Android debug build\n./gradlew :composeApp:assembleDebug\n\n# Desktop (run in dev mode)\n./gradlew :composeApp:run\n\n# Full build check (both platforms)\n./gradlew build\n\n# Lint (ktlint auto-formats on preBuild/compileKotlin* tasks automatically)\n./gradlew ktlintFormat           # manual format all modules\n./gradlew ktlintCheck            # check without fixing\n\n# Desktop installers\n./gradlew :composeApp:packageDmg    # macOS\n./gradlew :composeApp:packageExe    # Windows\n./gradlew :composeApp:packageDeb    # Linux\n```\n\n**Requirements:** JDK 21+ (Temurin recommended), Android SDK for Android builds.\n\n**Setup:** Create a GitHub OAuth App and put `GITHUB_CLIENT_ID=<your_id>` in `local.properties` (root). Callback URL: `githubstore://callback`.\n\n## Architecture\n\n**Clean Architecture + MVVM** with strict layer separation:\n\n- **Domain** — Repository interfaces, models, use cases. No framework dependencies.\n- **Data** — Repository implementations, Ktor API clients, Room DAOs, DTOs, mappers. Each feature's DI module lives in `data/di/SharedModule.kt`.\n- **Presentation** — ViewModels with `StateFlow`/`Channel`, Compose screens.\n\n### State Management Pattern (every screen)\n\nEvery ViewModel follows the same State/Action/Event pattern:\n\n- `State` — data class holding all UI state, exposed via `StateFlow`\n- `Action` — sealed interface for user input (clicks, refreshes)\n- `Event` — sealed interface for one-off effects (navigation, toasts), sent via `Channel.receiveAsFlow()`\n\n### Module Layout\n\n```text\ncomposeApp/          # App entry points, navigation, DI wiring\n  src/commonMain/    # Shared UI & wiring\n  src/androidMain/   # Android entry (MainActivity)\n  src/jvmMain/       # Desktop entry (DesktopApp.kt)\ncore/\n  domain/            # Shared interfaces, models, use cases\n  data/              # Networking (Ktor), database (Room), DI, platform impls\n  presentation/      # Material 3 theming, reusable UI components, localized strings (13 languages)\nfeature/<name>/\n  domain/            # Feature-specific interfaces & models\n  data/              # Feature-specific implementations & Koin DI module\n  presentation/      # Feature ViewModel + Compose screens\nbuild-logic/convention/  # Custom Gradle convention plugins\n```\n\nSome features (favourites, starred, recently-viewed, tweaks) are **presentation-only** — they use core repositories directly and register ViewModels in `composeApp/.../di/ViewModelsModule.kt` instead of having a `data/di/` layer.\n\n### Convention Plugins (build-logic)\n\n| Plugin ID | Use For |\n| :--- | :--- |\n| `convention.kmp.library` | KMP shared library modules (domain, data) |\n| `convention.cmp.library` | Compose Multiplatform library modules |\n| `convention.cmp.feature` | Feature presentation modules (auto-adds Compose + Koin + core:presentation) |\n| `convention.cmp.application` | Main app module |\n| `convention.room` | Room database modules |\n| `convention.buildkonfig` | Build-time config (reads from local.properties) |\n\n### Navigation\n\nType-safe navigation using `@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../navigation/GithubStoreGraph.kt`. Routes are wired in `AppNavigation.kt`. Parameterized routes: `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate)`, `DeveloperProfileScreen(username)`.\n\n### Dependency Injection\n\n**Koin** — each feature's data layer defines a module in `data/di/SharedModule.kt`. All modules are registered in `composeApp/.../di/initKoin.kt`. ViewModels injected via `koinViewModel()`. `DetailsViewModel` and `MirrorPickerViewModel` use manual Koin `viewModel { }` with `parametersOf()` for constructor args; all others use `viewModelOf(::ClassName)`.\n\n### Key Cross-Cutting Concerns\n\n- **Auth flow:** GitHub device-flow OAuth. Primary path goes through backend proxy (`/v1/auth/device/start`, `/v1/auth/device/poll`); falls back to direct GitHub only on infrastructure errors (5xx, timeouts). HTTP 4xx and GitHub's negative 200-bodies never trigger fallback. Backend rate limits (10 starts/hr, 200 polls/hr per IP) are hard — do not add retry loops.\n- **`X-GitHub-Token` header:** Forwarded on every backend passthrough route — `/v1/search`, `/v1/search/explore`, `/v1/repo/{owner}/{name}`, `/v1/releases/{owner}/{name}`, `/v1/readme/{owner}/{name}`, `/v1/user/{username}`. Backend re-sends as `Authorization: token $token` so upstream GitHub calls run under the user's 5000/hr OAuth quota; without it the request falls back to the shared 60/hr anonymous bucket and a single 4xx can poison the backend's 15-min negative cache for everyone. DB-only routes (`/v1/categories`, `/v1/topics`, `/v1/events`, `/v1/auth/device/*`, `/v1/badge/*`) never get the header. Sourced via `BackendApiClient.currentUserGithubToken()` (`private`), never logged. 401 from passthrough routes ≠ session expired — `AuthenticationStateImpl` debounces consecutive 401s under the same token before clearing the session.\n- **Platform branching:** Source sets are `commonMain` (shared), `androidMain` (Android), `jvmMain` (Desktop). Some features (apps, installation, Shizuku) are Android-only.\n- **Shizuku (Android):** Optional silent install via AIDL service. Falls back to standard installer on failure.\n\n## Coding Conventions\n\n- Packages: `zed.rainxch.{module}.{layer}` (e.g. `zed.rainxch.home.data.repository`)\n- Private state: underscore prefix `_state`, `_events`\n- Sealed classes/interfaces for type-safe routes, actions, events\n- Repository pattern: interface in `domain/`, implementation in `data/`\n- Ktlint auto-runs on `preBuild`/`compileKotlin*` tasks; `ignoreFailures = true`\n- Ktlint rules: wildcard imports allowed, filename rule disabled, `@Composable` functions exempt from function naming rule (see `.editorconfig`)\n\n## Adding a New Feature\n\n1. Create `feature/<name>/domain/`, `feature/<name>/data/`, `feature/<name>/presentation/`\n2. Add `build.gradle.kts` in each using the appropriate convention plugin\n3. Add `include` entries in `settings.gradle.kts`\n4. Define domain interfaces/models in `domain/`\n5. Implement repository + Koin DI module in `data/di/SharedModule.kt`\n6. Create ViewModel (State/Action/Event pattern) and Screen in `presentation/`\n7. Add navigation route to `GithubStoreGraph.kt` and wire in `AppNavigation.kt`\n8. Register the Koin module in `initKoin.kt`\n\n## Feature-Level Documentation\n\nEach `feature/` directory contains its own `CLAUDE.md` with module structure, key interfaces, navigation routes, and implementation notes. Read those for feature-specific guidance.\n\n## Versions\n\nAll library versions managed in `gradle/libs.versions.toml`. Key versions: Kotlin 2.3.10, Compose Multiplatform 1.10.3, Ktor 3.4.0, Room 2.8.4, Koin 4.1.1.\n","CLAUDE.md":"# Komi Store\n\nCross-platform app store for GitHub + Codeberg + Forgejo releases. **Kotlin Multiplatform** + **Compose Multiplatform**. Android (min API 26) + Desktop (JVM: Win/macOS/Linux). Package `zed.rainxch.githubstore`. Version 1.8.3 (code 18). Target SDK 36.\n\n## Build\n\n```bash\n./gradlew :composeApp:assembleDebug                                    # Android\n./gradlew :composeApp:run                                              # Desktop dev\n./gradlew :composeApp:packageExe :composeApp:packageMsi                # Win installer\n./gradlew :composeApp:packageDmg :composeApp:packagePkg                # macOS\n./gradlew :composeApp:packageDeb :composeApp:packageRpm                # Linux\n./gradlew build                                                        # full\n```\n\nJDK 21+. Android SDK for Android.\n\n## Structure\n\n```text\ncomposeApp/            # entry points, navigation, DI wiring (commonMain / androidMain / jvmMain)\ncore/\n  domain/              # interfaces, models, use cases (no framework deps)\n  data/                # repos, Ktor, Room, Koin, platform impls\n  presentation/        # Material 3 theme + reusable components + 14-locale strings\nfeature/\n  apps auth details dev-profile favourites homeP profile recently-viewed search starred tweaks\nbuild-logic/convention/  # convention plugins\n```\n\nEach feature: up to 3 sub-modules (`domain/`, `data/`, `presentation/`). `favourites`, `starred`, `recently-viewed` are presentation-only.\n\n## Architecture\n\nClean Architecture + MVVM. Layers: **Domain** (contracts), **Data** (Ktor + Room + Koin DI), **Presentation** (ViewModels with `StateFlow`/`Channel`, Compose).\n\n### State pattern (every screen)\n\n```kotlin\nclass XViewModel : ViewModel() {\n    private val _state = MutableStateFlow(XState())\n    val state = _state.asStateFlow()                  // or .stateIn(WhileSubscribed)\n    private val _events = Channel<XEvent>()\n    val events = _events.receiveAsFlow()\n    fun onAction(action: XAction) { ... }\n}\n```\n\n`State` = data class. `Action` = sealed (user input). `Event` = sealed (one-off effects).\n\n### Navigation\n\n`@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../app/navigation/`. Routes: `HomeScreen`, `SearchScreen`, `AuthenticationScreen`, `ProfileScreen`, `TweaksScreen`, `FavouritesScreen`, `StarredReposScreen`, `RecentlyViewedScreen`, `AppsScreen`, `OnboardingScreen`, `ExternalImportScreen`, `MirrorPickerScreen`, `StarredPickerScreen`, `SkippedUpdatesScreen`, `HiddenRepositoriesScreen`, `WhatsNewHistoryScreen`, `AnnouncementsScreen`, `HostTokensScreen`, `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate, sourceHost)`, `DeveloperProfileScreen(username)`. `DetailsScreen.sourceHost` is non-null for Codeberg / Forgejo / custom-forge repos — routes all `DetailsRepository` calls through `ForgejoClientRegistry` instead of the GitHub-backed default path.\n\n### DI\n\nKoin. Feature modules in `data/di/SharedModule.kt`. ViewModels in `composeApp/.../app/di/ViewModelsModule.kt` (`viewModelOf(::X)` or explicit `viewModel { ... }`). Wired in `initKoin.kt`.\n\n## Core repositories (`core/domain`)\n\n`FavouritesRepository`, `StarredRepository`, `InstalledAppsRepository`, `SeenReposRepository`, `HiddenReposRepository`, `SearchHistoryRepository`, `TweaksRepository`, `AuthenticationState`, `ThemesRepository`, `ProxyRepository`, `RateLimitRepository`, `ExternalImportRepository`, `TelemetryRepository`, `HostTokenRepository` (per-host PATs, KSafe-encrypted). Network: `ForgejoApiClient` + `ForgejoClientRegistry` (per-host Ktor clients, thread-safe via Mutex, proxy-aware, closes cached engines on shutdown / proxy change). Util: `AssetVariant` (token/glob/stem fingerprinting), `assetPlatformOf`, `RepoIdCodec` (23-bit host fingerprint + 40-bit raw id packed into the existing 64-bit `repoId` slot — sign bit = foreign source), `RepositoryUrlParser` (recognises GitHub + Codeberg + gitea.com + git.disroot.org + user-added forge hosts). System interfaces: `Installer`, `InstallerStatusProvider`, `PackageMonitor`, `SystemInstallSerializer`.\n\n## Tech\n\nKotlin 2.3.10, Compose Multiplatform 1.10.3, Ktor 3.4.0, Room 2.8.4, Koin 4.1.1, kotlinx.serialization 1.10.0, DataStore 1.2.0, Landscapist 2.9.5, Kermit 2.0.8, MOKO Permissions 0.20.1, Navigation Compose 2.9.2, multiplatform-markdown-renderer 0.39.2, Shizuku 13.1.5, WorkManager 2.11.1, kotlinx.datetime 0.7.1. Versions in `gradle/libs.versions.toml`.\n\n## Convention plugins (`build-logic/convention/`)\n\n`convention.kmp.library` (domain/data), `convention.cmp.library` (core/presentation), `convention.cmp.feature` (feature presentation), `convention.cmp.application` (main app), `convention.room`, `convention.buildkonfig`.\n\n## Adding a feature\n\n1. `feature/<name>/{domain,data,presentation}/` with appropriate convention plugin\n2. `include` in `settings.gradle.kts`\n3. Domain interfaces → impl + Koin module in `data/di/SharedModule.kt` → ViewModel + Screen\n4. Route in `GithubStoreGraph.kt` + wire in `AppNavigation.kt` + register Koin in `initKoin.kt`\n\n## Key configuration\n\n- **GitHub OAuth:** `GITHUB_CLIENT_ID` in `local.properties`. Deep links: `githubstore://auth` (web-OAuth handoff), `githubstore://callback` (legacy device-flow leftover), `githubstore://repo`, `githubstore://apps`.\n- **Shizuku (Android):** silent install via `ShizukuProvider` → AIDL → `pm install -S`. Fallback to standard installer on failure.\n- **Desktop logs:** `CrashReporter` (first line of `DesktopApp.main`) tees stdout/stderr to rotating `session.log` + writes `crash-<ts>.log` on uncaught. Paths: `~/Library/Logs/GitHub-Store/` (macOS), `%LOCALAPPDATA%/GitHub-Store/logs/` (Win), `$XDG_STATE_HOME/GitHub-Store/logs/` (Linux). Android = Logcat.\n- **macOS distribution:** Homebrew cask in tap `openhub-store/tap` (separate repo `homebrew-tap`). `brew install --cask github-store`. Unsigned at present — user must `xattr -dr com.apple.quarantine /Applications/GitHub-Store.app` after install. CI builds `.dmg` + `.pkg` on every push to `generate-installers`; tap cask updates automatically on release.\n- **`X-GitHub-Token` header:** Client attaches when `TokenStore.currentToken()` is non-null on `/v1/search`, `/v1/search/explore`, `/v1/repo`, `/v1/releases`, `/v1/readme`, `/v1/user`. Backend re-sends as `Authorization: token $token` to GitHub. Without it, backend round-robins a 4-token service pool. Upstream 401 remapped to backend `502` (handled like \"GitHub unreachable\" — fall back via `shouldFallbackToGithubOrRethrow`). `429` = no fallback (same wall), only backoff. `UnauthorizedInterceptor` only on direct-GitHub client; `AuthenticationStateImpl` debounces consecutive 401s by token snapshot.\n- **Auth flow (web-OAuth-first):** Primary path is web OAuth with PKCE + handoff. `feature/auth/data/crypto/PkceGenerator` mints `(state, codeVerifier, codeChallenge)`; `WebAuthApi.register` POSTs verifier + challenge + state to `https://github-store.org/auth/register` (Cloudflare Worker stashes them in Workers KV) and returns `authUrl`. User opens it, authorizes on `github.com`, GitHub redirects to `github-store.org/auth/callback?code&state` where the Worker exchanges the code via `api.github-store.org` (backend stores `(handoffId → access_token)` for 60s in Postgres with atomic `DELETE…RETURNING`), then bounces back to `githubstore://auth?h=<handoffId>`. App reads handoff via `WebAuthApi.consumeHandoff` (GETDEL semantics). Secondary path: device flow via backend `/v1/auth/device/start` + `/poll`, `AuthPath` (`Backend`|`Direct`) tracked in `SavedStateHandle`, only escalates `Backend → Direct` on infra errors. Tertiary: paste a Personal Access Token (`signInWithPat` — validates against `/user`, persists optimistically when GitHub unreachable). Backend rate limits: 10 device-starts/hr, 200 device-polls/hr per IP. Endpoints in `core/data/network/BackendEndpoints.kt` (`BACKEND_ORIGIN`, `WEB_ORIGIN`).\n- **Windows installer signing (SignPath Foundation):** CI workflow `.github/workflows/build-desktop-platforms.yml` job `sign-windows` after every push to `generate-installers` branch. Action pinned to commit SHA (not `@v2`). Secrets: `SIGNPATH_API_TOKEN`, `SIGNPATH_ORGANIZATION_ID` (`1ecf111e-...`). Variable `SIGNPATH_SIGNING_POLICY_SLUG` = `test-signing` until prod cert issued; flip to `release-signing`. Project slug `GitHub-Store`, artifact config slug `initial`. Unsigned artifact deleted post-sign; only `windows-installers-signed` reaches the draft release.\n- **WinGet publish:** `.github/workflows/winget-publish.yml` fires on `release: [released]`. Action `vedantmgoyal9/winget-releaser@main`. Secret `WINGET_TOKEN` = PAT with `Contents+Pull requests: write` on `OpenHub-Store/winget-pkgs` (fork of `microsoft/winget-pkgs`). Pin `fork-user: OpenHub-Store` explicitly so the action doesn't infer from token owner.\n- **Forges (Codeberg / Forgejo / Gitea):** `ForgejoApiClient` per host (60s req / 30s connect+socket timeouts, exponential retry on 5xx + IOException). `ForgejoClientRegistry.clientFor(host)` cached + Mutex-guarded. Direct-to-forge — no backend mediator. `RepoIdCodec` packs host fingerprint into `repoId` so the existing GitHub-shaped schema survives. README via `/contents/README.md?ref={branch}` (Forgejo has NO `/readme` endpoint). License sniffed from `/contents/LICENSE` regex against SPDX headers. Downloads aggregated by summing `asset.download_count` across releases.\n- **Per-host PATs:** `HostTokenRepository` stores `{host, token, label, createdAt}` rows AES-256-GCM encrypted via KSafe. `HostTokenInterceptor` (Ktor plugin) injects `Authorization: token $pat` on matched host. `HostNames.apiHostToTokenHost` maps `api.github.com → github.com` so the GitHub-direct client looks up the right PAT. UI at `Tweaks → Access Tokens` (`HostTokensScreen`).\n- **KSafe:** AES-256-GCM with hardware-backed Keystore on Android. Wraps every persisted credential / pref via `core/data/secure/KSafeSafe.kt` extension funcs (`safeGet`, `safePut`, `safeDelete`, `safeGetFlow`) — surface log + return null/false on transient failure instead of throwing through coroutine scopes.\n- **Translation providers:** `TranslationProvider` enum = `GOOGLE`, `YOUDAO`, `LIBRE_TRANSLATE`, `DEEPL`, `MICROSOFT`. Each per-provider config persisted via `TweaksRepository` (KSafe-encrypted). `TranslationRepositoryImpl.resolveTranslator()` picks the impl. LibreTranslate defaults to the bundled `translate.disroot.org` mirror when user URL pref blank. DeepL auto-routes `:fx`-suffixed keys to `api-free.deepl.com`. Microsoft uses No-Trace by default — text never stored, never used for training.\n- **Gradle:** Config + build cache enabled. 4GB Gradle heap, 3GB Kotlin daemon. Official Kotlin style.\n\n## Active skills (apply on matching domain)\n\n- **caveman** — session default, terse output.\n- **karpathy-guidelines** — anti-overcomplication, minimal diffs, surface assumptions, verifiable success criteria. Every coding task.\n- **one-skill-to-rule-them-all** — watch for skill-capture opportunities during multi-step work.\n- **gsd-inbox** - Triage open GitHub issues + PRs against templates. Our exact pattern — automate the \"check issue #N, draft reply, ship fix\" loop.\n- **gsd-ship** - Create PR + review + prep for merge. Every task ends here.\n- **gsd-quick** - Trivial task with atomic commits + state tracking. Matches our small-commit policy.\n- **gsd-debug** - Systematic debugging with persistent state across context resets. For bug-hunt cycles.\n- **android-* skills** (`~/.claude/skills/android/`) — auto-fire by description match; apply when in matching domain:\n  - `android-compose-ui` — composables, recomposition, animations, modifiers, design system\n  - `android-data-layer` — repos, DTOs, Room, Ktor, mappers\n  - `android-di-koin` — Koin module setup, ViewModel injection\n  - `android-error-handling` — Result wrapper, typed errors\n  - `android-module-structure` — feature-layered modules, convention plugins\n  - `android-navigation` — type-safe Compose nav\n  - `android-presentation-mvi` — State/Action/Event, Root/Screen split, UiText, SavedStateHandle\n  - `android-testing` — testing patterns\n\n## Conventions\n\n- Packages `zed.rainxch.{module}.{layer}`\n- Private state fields prefix `_state`\n- Sealed routes/actions/events\n- Repository pattern: interface in `domain/`, impl in `data/`\n- Source sets: `commonMain` shared, `androidMain`, `jvmMain`\n- **No KDoc, no inline comments** unless the user explicitly asks. No function/class docs. Inline only for non-obvious invariants, tricky concurrency, workarounds. Applies globally.\n- Feature-specific guidance in each `feature/*/CLAUDE.md`\n\n## Approach\n\n- Read existing files before writing. Don't re-read unless changed.\n- Thorough in reasoning, concise in output.\n- Skip files over 100KB unless required.\n- No sycophantic openers or closing fluff.\n- No emojis or em-dashes.\n- Do not guess APIs, versions, flags, commit SHAs, or package names. Verify by reading code or docs before asserting, researching if necessary.\n"},"files":{"AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to WARP (warp.dev) when working with code in this repository.\n\n## Project Overview\n\nKomi Store is a cross-platform app store for GitHub releases built with **Kotlin Multiplatform (KMP)** and **Compose Multiplatform**. It targets **Android** (min API 26, target 36) and **Desktop** (Windows, macOS, Linux via JVM).\n\nPackage: `zed.rainxch.githubstore`\n\n## Build & Run Commands\n\n```bash\n# Android debug build\n./gradlew :composeApp:assembleDebug\n\n# Desktop (run in dev mode)\n./gradlew :composeApp:run\n\n# Full build check (both platforms)\n./gradlew build\n\n# Lint (ktlint auto-formats on preBuild/compileKotlin* tasks automatically)\n./gradlew ktlintFormat           # manual format all modules\n./gradlew ktlintCheck            # check without fixing\n\n# Desktop installers\n./gradlew :composeApp:packageDmg    # macOS\n./gradlew :composeApp:packageExe    # Windows\n./gradlew :composeApp:packageDeb    # Linux\n```\n\n**Requirements:** JDK 21+ (Temurin recommended), Android SDK for Android builds.\n\n**Setup:** Create a GitHub OAuth App and put `GITHUB_CLIENT_ID=<your_id>` in `local.properties` (root). Callback URL: `githubstore://callback`.\n\n## Architecture\n\n**Clean Architecture + MVVM** with strict layer separation:\n\n- **Domain** — Repository interfaces, models, use cases. No framework dependencies.\n- **Data** — Repository implementations, Ktor API clients, Room DAOs, DTOs, mappers. Each feature's DI module lives in `data/di/SharedModule.kt`.\n- **Presentation** — ViewModels with `StateFlow`/`Channel`, Compose screens.\n\n### State Management Pattern (every screen)\n\nEvery ViewModel follows the same State/Action/Event pattern:\n\n- `State` — data class holding all UI state, exposed via `StateFlow`\n- `Action` — sealed interface for user input (clicks, refreshes)\n- `Event` — sealed interface for one-off effects (navigation, toasts), sent via `Channel.receiveAsFlow()`\n\n### Module Layout\n\n```text\ncomposeApp/          # App entry points, navigation, DI wiring\n  src/commonMain/    # Shared UI & wiring\n  src/androidMain/   # Android entry (MainActivity)\n  src/jvmMain/       # Desktop entry (DesktopApp.kt)\ncore/\n  domain/            # Shared interfaces, models, use cases\n  data/              # Networking (Ktor), database (Room), DI, platform impls\n  presentation/      # Material 3 theming, reusable UI components, localized strings (13 languages)\nfeature/<name>/\n  domain/            # Feature-specific interfaces & models\n  data/              # Feature-specific implementations & Koin DI module\n  presentation/      # Feature ViewModel + Compose screens\nbuild-logic/convention/  # Custom Gradle convention plugins\n```\n\nSome features (favourites, starred, recently-viewed, tweaks) are **presentation-only** — they use core repositories directly and register ViewModels in `composeApp/.../di/ViewModelsModule.kt` instead of having a `data/di/` layer.\n\n### Convention Plugins (build-logic)\n\n| Plugin ID | Use For |\n| :--- | :--- |\n| `convention.kmp.library` | KMP shared library modules (domain, data) |\n| `convention.cmp.library` | Compose Multiplatform library modules |\n| `convention.cmp.feature` | Feature presentation modules (auto-adds Compose + Koin + core:presentation) |\n| `convention.cmp.application` | Main app module |\n| `convention.room` | Room database modules |\n| `convention.buildkonfig` | Build-time config (reads from local.properties) |\n\n### Navigation\n\nType-safe navigation using `@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../navigation/GithubStoreGraph.kt`. Routes are wired in `AppNavigation.kt`. Parameterized routes: `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate)`, `DeveloperProfileScreen(username)`.\n\n### Dependency Injection\n\n**Koin** — each feature's data layer defines a module in `data/di/SharedModule.kt`. All modules are registered in `composeApp/.../di/initKoin.kt`. ViewModels injected via `koinViewModel()`. `DetailsViewModel` and `MirrorPickerViewModel` use manual Koin `viewModel { }` with `parametersOf()` for constructor args; all others use `viewModelOf(::ClassName)`.\n\n### Key Cross-Cutting Concerns\n\n- **Auth flow:** GitHub device-flow OAuth. Primary path goes through backend proxy (`/v1/auth/device/start`, `/v1/auth/device/poll`); falls back to direct GitHub only on infrastructure errors (5xx, timeouts). HTTP 4xx and GitHub's negative 200-bodies never trigger fallback. Backend rate limits (10 starts/hr, 200 polls/hr per IP) are hard — do not add retry loops.\n- **`X-GitHub-Token` header:** Forwarded on every backend passthrough route — `/v1/search`, `/v1/search/explore`, `/v1/repo/{owner}/{name}`, `/v1/releases/{owner}/{name}`, `/v1/readme/{owner}/{name}`, `/v1/user/{username}`. Backend re-sends as `Authorization: token $token` so upstream GitHub calls run under the user's 5000/hr OAuth quota; without it the request falls back to the shared 60/hr anonymous bucket and a single 4xx can poison the backend's 15-min negative cache for everyone. DB-only routes (`/v1/categories`, `/v1/topics`, `/v1/events`, `/v1/auth/device/*`, `/v1/badge/*`) never get the header. Sourced via `BackendApiClient.currentUserGithubToken()` (`private`), never logged. 401 from passthrough routes ≠ session expired — `AuthenticationStateImpl` debounces consecutive 401s under the same token before clearing the session.\n- **Platform branching:** Source sets are `commonMain` (shared), `androidMain` (Android), `jvmMain` (Desktop). Some features (apps, installation, Shizuku) are Android-only.\n- **Shizuku (Android):** Optional silent install via AIDL service. Falls back to standard installer on failure.\n\n## Coding Conventions\n\n- Packages: `zed.rainxch.{module}.{layer}` (e.g. `zed.rainxch.home.data.repository`)\n- Private state: underscore prefix `_state`, `_events`\n- Sealed classes/interfaces for type-safe routes, actions, events\n- Repository pattern: interface in `domain/`, implementation in `data/`\n- Ktlint auto-runs on `preBuild`/`compileKotlin*` tasks; `ignoreFailures = true`\n- Ktlint rules: wildcard imports allowed, filename rule disabled, `@Composable` functions exempt from function naming rule (see `.editorconfig`)\n\n## Adding a New Feature\n\n1. Create `feature/<name>/domain/`, `feature/<name>/data/`, `feature/<name>/presentation/`\n2. Add `build.gradle.kts` in each using the appropriate convention plugin\n3. Add `include` entries in `settings.gradle.kts`\n4. Define domain interfaces/models in `domain/`\n5. Implement repository + Koin DI module in `data/di/SharedModule.kt`\n6. Create ViewModel (State/Action/Event pattern) and Screen in `presentation/`\n7. Add navigation route to `GithubStoreGraph.kt` and wire in `AppNavigation.kt`\n8. Register the Koin module in `initKoin.kt`\n\n## Feature-Level Documentation\n\nEach `feature/` directory contains its own `CLAUDE.md` with module structure, key interfaces, navigation routes, and implementation notes. Read those for feature-specific guidance.\n\n## Versions\n\nAll library versions managed in `gradle/libs.versions.toml`. Key versions: Kotlin 2.3.10, Compose Multiplatform 1.10.3, Ktor 3.4.0, Room 2.8.4, Koin 4.1.1.\n","CLAUDE.md":"# Komi Store\n\nCross-platform app store for GitHub + Codeberg + Forgejo releases. **Kotlin Multiplatform** + **Compose Multiplatform**. Android (min API 26) + Desktop (JVM: Win/macOS/Linux). Package `zed.rainxch.githubstore`. Version 1.8.3 (code 18). Target SDK 36.\n\n## Build\n\n```bash\n./gradlew :composeApp:assembleDebug                                    # Android\n./gradlew :composeApp:run                                              # Desktop dev\n./gradlew :composeApp:packageExe :composeApp:packageMsi                # Win installer\n./gradlew :composeApp:packageDmg :composeApp:packagePkg                # macOS\n./gradlew :composeApp:packageDeb :composeApp:packageRpm                # Linux\n./gradlew build                                                        # full\n```\n\nJDK 21+. Android SDK for Android.\n\n## Structure\n\n```text\ncomposeApp/            # entry points, navigation, DI wiring (commonMain / androidMain / jvmMain)\ncore/\n  domain/              # interfaces, models, use cases (no framework deps)\n  data/                # repos, Ktor, Room, Koin, platform impls\n  presentation/        # Material 3 theme + reusable components + 14-locale strings\nfeature/\n  apps auth details dev-profile favourites homeP profile recently-viewed search starred tweaks\nbuild-logic/convention/  # convention plugins\n```\n\nEach feature: up to 3 sub-modules (`domain/`, `data/`, `presentation/`). `favourites`, `starred`, `recently-viewed` are presentation-only.\n\n## Architecture\n\nClean Architecture + MVVM. Layers: **Domain** (contracts), **Data** (Ktor + Room + Koin DI), **Presentation** (ViewModels with `StateFlow`/`Channel`, Compose).\n\n### State pattern (every screen)\n\n```kotlin\nclass XViewModel : ViewModel() {\n    private val _state = MutableStateFlow(XState())\n    val state = _state.asStateFlow()                  // or .stateIn(WhileSubscribed)\n    private val _events = Channel<XEvent>()\n    val events = _events.receiveAsFlow()\n    fun onAction(action: XAction) { ... }\n}\n```\n\n`State` = data class. `Action` = sealed (user input). `Event` = sealed (one-off effects).\n\n### Navigation\n\n`@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../app/navigation/`. Routes: `HomeScreen`, `SearchScreen`, `AuthenticationScreen`, `ProfileScreen`, `TweaksScreen`, `FavouritesScreen`, `StarredReposScreen`, `RecentlyViewedScreen`, `AppsScreen`, `OnboardingScreen`, `ExternalImportScreen`, `MirrorPickerScreen`, `StarredPickerScreen`, `SkippedUpdatesScreen`, `HiddenRepositoriesScreen`, `WhatsNewHistoryScreen`, `AnnouncementsScreen`, `HostTokensScreen`, `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate, sourceHost)`, `DeveloperProfileScreen(username)`. `DetailsScreen.sourceHost` is non-null for Codeberg / Forgejo / custom-forge repos — routes all `DetailsRepository` calls through `ForgejoClientRegistry` instead of the GitHub-backed default path.\n\n### DI\n\nKoin. Feature modules in `data/di/SharedModule.kt`. ViewModels in `composeApp/.../app/di/ViewModelsModule.kt` (`viewModelOf(::X)` or explicit `viewModel { ... }`). Wired in `initKoin.kt`.\n\n## Core repositories (`core/domain`)\n\n`FavouritesRepository`, `StarredRepository`, `InstalledAppsRepository`, `SeenReposRepository`, `HiddenReposRepository`, `SearchHistoryRepository`, `TweaksRepository`, `AuthenticationState`, `ThemesRepository`, `ProxyRepository`, `RateLimitRepository`, `ExternalImportRepository`, `TelemetryRepository`, `HostTokenRepository` (per-host PATs, KSafe-encrypted). Network: `ForgejoApiClient` + `ForgejoClientRegistry` (per-host Ktor clients, thread-safe via Mutex, proxy-aware, closes cached engines on shutdown / proxy change). Util: `AssetVariant` (token/glob/stem fingerprinting), `assetPlatformOf`, `RepoIdCodec` (23-bit host fingerprint + 40-bit raw id packed into the existing 64-bit `repoId` slot — sign bit = foreign source), `RepositoryUrlParser` (recognises GitHub + Codeberg + gitea.com + git.disroot.org + user-added forge hosts). System interfaces: `Installer`, `InstallerStatusProvider`, `PackageMonitor`, `SystemInstallSerializer`.\n\n## Tech\n\nKotlin 2.3.10, Compose Multiplatform 1.10.3, Ktor 3.4.0, Room 2.8.4, Koin 4.1.1, kotlinx.serialization 1.10.0, DataStore 1.2.0, Landscapist 2.9.5, Kermit 2.0.8, MOKO Permissions 0.20.1, Navigation Compose 2.9.2, multiplatform-markdown-renderer 0.39.2, Shizuku 13.1.5, WorkManager 2.11.1, kotlinx.datetime 0.7.1. Versions in `gradle/libs.versions.toml`.\n\n## Convention plugins (`build-logic/convention/`)\n\n`convention.kmp.library` (domain/data), `convention.cmp.library` (core/presentation), `convention.cmp.feature` (feature presentation), `convention.cmp.application` (main app), `convention.room`, `convention.buildkonfig`.\n\n## Adding a feature\n\n1. `feature/<name>/{domain,data,presentation}/` with appropriate convention plugin\n2. `include` in `settings.gradle.kts`\n3. Domain interfaces → impl + Koin module in `data/di/SharedModule.kt` → ViewModel + Screen\n4. Route in `GithubStoreGraph.kt` + wire in `AppNavigation.kt` + register Koin in `initKoin.kt`\n\n## Key configuration\n\n- **GitHub OAuth:** `GITHUB_CLIENT_ID` in `local.properties`. Deep links: `githubstore://auth` (web-OAuth handoff), `githubstore://callback` (legacy device-flow leftover), `githubstore://repo`, `githubstore://apps`.\n- **Shizuku (Android):** silent install via `ShizukuProvider` → AIDL → `pm install -S`. Fallback to standard installer on failure.\n- **Desktop logs:** `CrashReporter` (first line of `DesktopApp.main`) tees stdout/stderr to rotating `session.log` + writes `crash-<ts>.log` on uncaught. Paths: `~/Library/Logs/GitHub-Store/` (macOS), `%LOCALAPPDATA%/GitHub-Store/logs/` (Win), `$XDG_STATE_HOME/GitHub-Store/logs/` (Linux). Android = Logcat.\n- **macOS distribution:** Homebrew cask in tap `openhub-store/tap` (separate repo `homebrew-tap`). `brew install --cask github-store`. Unsigned at present — user must `xattr -dr com.apple.quarantine /Applications/GitHub-Store.app` after install. CI builds `.dmg` + `.pkg` on every push to `generate-installers`; tap cask updates automatically on release.\n- **`X-GitHub-Token` header:** Client attaches when `TokenStore.currentToken()` is non-null on `/v1/search`, `/v1/search/explore`, `/v1/repo`, `/v1/releases`, `/v1/readme`, `/v1/user`. Backend re-sends as `Authorization: token $token` to GitHub. Without it, backend round-robins a 4-token service pool. Upstream 401 remapped to backend `502` (handled like \"GitHub unreachable\" — fall back via `shouldFallbackToGithubOrRethrow`). `429` = no fallback (same wall), only backoff. `UnauthorizedInterceptor` only on direct-GitHub client; `AuthenticationStateImpl` debounces consecutive 401s by token snapshot.\n- **Auth flow (web-OAuth-first):** Primary path is web OAuth with PKCE + handoff. `feature/auth/data/crypto/PkceGenerator` mints `(state, codeVerifier, codeChallenge)`; `WebAuthApi.register` POSTs verifier + challenge + state to `https://github-store.org/auth/register` (Cloudflare Worker stashes them in Workers KV) and returns `authUrl`. User opens it, authorizes on `github.com`, GitHub redirects to `github-store.org/auth/callback?code&state` where the Worker exchanges the code via `api.github-store.org` (backend stores `(handoffId → access_token)` for 60s in Postgres with atomic `DELETE…RETURNING`), then bounces back to `githubstore://auth?h=<handoffId>`. App reads handoff via `WebAuthApi.consumeHandoff` (GETDEL semantics). Secondary path: device flow via backend `/v1/auth/device/start` + `/poll`, `AuthPath` (`Backend`|`Direct`) tracked in `SavedStateHandle`, only escalates `Backend → Direct` on infra errors. Tertiary: paste a Personal Access Token (`signInWithPat` — validates against `/user`, persists optimistically when GitHub unreachable). Backend rate limits: 10 device-starts/hr, 200 device-polls/hr per IP. Endpoints in `core/data/network/BackendEndpoints.kt` (`BACKEND_ORIGIN`, `WEB_ORIGIN`).\n- **Windows installer signing (SignPath Foundation):** CI workflow `.github/workflows/build-desktop-platforms.yml` job `sign-windows` after every push to `generate-installers` branch. Action pinned to commit SHA (not `@v2`). Secrets: `SIGNPATH_API_TOKEN`, `SIGNPATH_ORGANIZATION_ID` (`1ecf111e-...`). Variable `SIGNPATH_SIGNING_POLICY_SLUG` = `test-signing` until prod cert issued; flip to `release-signing`. Project slug `GitHub-Store`, artifact config slug `initial`. Unsigned artifact deleted post-sign; only `windows-installers-signed` reaches the draft release.\n- **WinGet publish:** `.github/workflows/winget-publish.yml` fires on `release: [released]`. Action `vedantmgoyal9/winget-releaser@main`. Secret `WINGET_TOKEN` = PAT with `Contents+Pull requests: write` on `OpenHub-Store/winget-pkgs` (fork of `microsoft/winget-pkgs`). Pin `fork-user: OpenHub-Store` explicitly so the action doesn't infer from token owner.\n- **Forges (Codeberg / Forgejo / Gitea):** `ForgejoApiClient` per host (60s req / 30s connect+socket timeouts, exponential retry on 5xx + IOException). `ForgejoClientRegistry.clientFor(host)` cached + Mutex-guarded. Direct-to-forge — no backend mediator. `RepoIdCodec` packs host fingerprint into `repoId` so the existing GitHub-shaped schema survives. README via `/contents/README.md?ref={branch}` (Forgejo has NO `/readme` endpoint). License sniffed from `/contents/LICENSE` regex against SPDX headers. Downloads aggregated by summing `asset.download_count` across releases.\n- **Per-host PATs:** `HostTokenRepository` stores `{host, token, label, createdAt}` rows AES-256-GCM encrypted via KSafe. `HostTokenInterceptor` (Ktor plugin) injects `Authorization: token $pat` on matched host. `HostNames.apiHostToTokenHost` maps `api.github.com → github.com` so the GitHub-direct client looks up the right PAT. UI at `Tweaks → Access Tokens` (`HostTokensScreen`).\n- **KSafe:** AES-256-GCM with hardware-backed Keystore on Android. Wraps every persisted credential / pref via `core/data/secure/KSafeSafe.kt` extension funcs (`safeGet`, `safePut`, `safeDelete`, `safeGetFlow`) — surface log + return null/false on transient failure instead of throwing through coroutine scopes.\n- **Translation providers:** `TranslationProvider` enum = `GOOGLE`, `YOUDAO`, `LIBRE_TRANSLATE`, `DEEPL`, `MICROSOFT`. Each per-provider config persisted via `TweaksRepository` (KSafe-encrypted). `TranslationRepositoryImpl.resolveTranslator()` picks the impl. LibreTranslate defaults to the bundled `translate.disroot.org` mirror when user URL pref blank. DeepL auto-routes `:fx`-suffixed keys to `api-free.deepl.com`. Microsoft uses No-Trace by default — text never stored, never used for training.\n- **Gradle:** Config + build cache enabled. 4GB Gradle heap, 3GB Kotlin daemon. Official Kotlin style.\n\n## Active skills (apply on matching domain)\n\n- **caveman** — session default, terse output.\n- **karpathy-guidelines** — anti-overcomplication, minimal diffs, surface assumptions, verifiable success criteria. Every coding task.\n- **one-skill-to-rule-them-all** — watch for skill-capture opportunities during multi-step work.\n- **gsd-inbox** - Triage open GitHub issues + PRs against templates. Our exact pattern — automate the \"check issue #N, draft reply, ship fix\" loop.\n- **gsd-ship** - Create PR + review + prep for merge. Every task ends here.\n- **gsd-quick** - Trivial task with atomic commits + state tracking. Matches our small-commit policy.\n- **gsd-debug** - Systematic debugging with persistent state across context resets. For bug-hunt cycles.\n- **android-* skills** (`~/.claude/skills/android/`) — auto-fire by description match; apply when in matching domain:\n  - `android-compose-ui` — composables, recomposition, animations, modifiers, design system\n  - `android-data-layer` — repos, DTOs, Room, Ktor, mappers\n  - `android-di-koin` — Koin module setup, ViewModel injection\n  - `android-error-handling` — Result wrapper, typed errors\n  - `android-module-structure` — feature-layered modules, convention plugins\n  - `android-navigation` — type-safe Compose nav\n  - `android-presentation-mvi` — State/Action/Event, Root/Screen split, UiText, SavedStateHandle\n  - `android-testing` — testing patterns\n\n## Conventions\n\n- Packages `zed.rainxch.{module}.{layer}`\n- Private state fields prefix `_state`\n- Sealed routes/actions/events\n- Repository pattern: interface in `domain/`, impl in `data/`\n- Source sets: `commonMain` shared, `androidMain`, `jvmMain`\n- **No KDoc, no inline comments** unless the user explicitly asks. No function/class docs. Inline only for non-obvious invariants, tricky concurrency, workarounds. Applies globally.\n- Feature-specific guidance in each `feature/*/CLAUDE.md`\n\n## Approach\n\n- Read existing files before writing. Don't re-read unless changed.\n- Thorough in reasoning, concise in output.\n- Skip files over 100KB unless required.\n- No sycophantic openers or closing fluff.\n- No emojis or em-dashes.\n- Do not guess APIs, versions, flags, commit SHAs, or package names. Verify by reading code or docs before asserting, researching if necessary.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\nThis file provides guidance to WARP (warp.dev) when working with code in this repository.\n\n## Project Overview\n\nKomi Store is a cross-platform app store for GitHub releases built with **Kotlin Multiplatform (KMP)** and **Compose Multiplatform**. It targets **Android** (min API 26, target 36) and **Desktop** (Windows, macOS, Linux via JVM).\n\nPackage: `zed.rainxch.githubstore`\n\n## Build & Run Commands\n\n```bash\n# Android debug build\n./gradlew :composeApp:assembleDebug\n\n# Desktop (run in dev mode)\n./gradlew :composeApp:run\n\n# Full build check (both platforms)\n./gradlew build\n\n# Lint (ktlint auto-formats on preBuild/compileKotlin* tasks automatically)\n./gradlew ktlintFormat           # manual format all modules\n./gradlew ktlintCheck            # check without fixing\n\n# Desktop installers\n./gradlew :composeApp:packageDmg    # macOS\n./gradlew :composeApp:packageExe    # Windows\n./gradlew :composeApp:packageDeb    # Linux\n```\n\n**Requirements:** JDK 21+ (Temurin recommended), Android SDK for Android builds.\n\n**Setup:** Create a GitHub OAuth App and put `GITHUB_CLIENT_ID=<your_id>` in `local.properties` (root). Callback URL: `githubstore://callback`.\n\n## Architecture\n\n**Clean Architecture + MVVM** with strict layer separation:\n\n- **Domain** — Repository interfaces, models, use cases. No framework dependencies.\n- **Data** — Repository implementations, Ktor API clients, Room DAOs, DTOs, mappers. Each feature's DI module lives in `data/di/SharedModule.kt`.\n- **Presentation** — ViewModels with `StateFlow`/`Channel`, Compose screens.\n\n### State Management Pattern (every screen)\n\nEvery ViewModel follows the same State/Action/Event pattern:\n\n- `State` — data class holding all UI state, exposed via `StateFlow`\n- `Action` — sealed interface for user input (clicks, refreshes)\n- `Event` — sealed interface for one-off effects (navigation, toasts), sent via `Channel.receiveAsFlow()`\n\n### Module Layout\n\n```text\ncomposeApp/          # App entry points, navigation, DI wiring\n  src/commonMain/    # Shared UI & wiring\n  src/androidMain/   # Android entry (MainActivity)\n  src/jvmMain/       # Desktop entry (DesktopApp.kt)\ncore/\n  domain/            # Shared interfaces, models, use cases\n  data/              # Networking (Ktor), database (Room), DI, platform impls\n  presentation/      # Material 3 theming, reusable UI components, localized strings (13 languages)\nfeature/<name>/\n  domain/            # Feature-specific interfaces & models\n  data/              # Feature-specific implementations & Koin DI module\n  presentation/      # Feature ViewModel + Compose screens\nbuild-logic/convention/  # Custom Gradle convention plugins\n```\n\nSome features (favourites, starred, recently-viewed, tweaks) are **presentation-only** — they use core repositories directly and register ViewModels in `composeApp/.../di/ViewModelsModule.kt` instead of having a `data/di/` layer.\n\n### Convention Plugins (build-logic)\n\n| Plugin ID | Use For |\n| :--- | :--- |\n| `convention.kmp.library` | KMP shared library modules (domain, data) |\n| `convention.cmp.library` | Compose Multiplatform library modules |\n| `convention.cmp.feature` | Feature presentation modules (auto-adds Compose + Koin + core:presentation) |\n| `convention.cmp.application` | Main app module |\n| `convention.room` | Room database modules |\n| `convention.buildkonfig` | Build-time config (reads from local.properties) |\n\n### Navigation\n\nType-safe navigation using `@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../navigation/GithubStoreGraph.kt`. Routes are wired in `AppNavigation.kt`. Parameterized routes: `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate)`, `DeveloperProfileScreen(username)`.\n\n### Dependency Injection\n\n**Koin** — each feature's data layer defines a module in `data/di/SharedModule.kt`. All modules are registered in `composeApp/.../di/initKoin.kt`. ViewModels injected via `koinViewModel()`. `DetailsViewModel` and `MirrorPickerViewModel` use manual Koin `viewModel { }` with `parametersOf()` for constructor args; all others use `viewModelOf(::ClassName)`.\n\n### Key Cross-Cutting Concerns\n\n- **Auth flow:** GitHub device-flow OAuth. Primary path goes through backend proxy (`/v1/auth/device/start`, `/v1/auth/device/poll`); falls back to direct GitHub only on infrastructure errors (5xx, timeouts). HTTP 4xx and GitHub's negative 200-bodies never trigger fallback. Backend rate limits (10 starts/hr, 200 polls/hr per IP) are hard — do not add retry loops.\n- **`X-GitHub-Token` header:** Forwarded on every backend passthrough route — `/v1/search`, `/v1/search/explore`, `/v1/repo/{owner}/{name}`, `/v1/releases/{owner}/{name}`, `/v1/readme/{owner}/{name}`, `/v1/user/{username}`. Backend re-sends as `Authorization: token $token` so upstream GitHub calls run under the user's 5000/hr OAuth quota; without it the request falls back to the shared 60/hr anonymous bucket and a single 4xx can poison the backend's 15-min negative cache for everyone. DB-only routes (`/v1/categories`, `/v1/topics`, `/v1/events`, `/v1/auth/device/*`, `/v1/badge/*`) never get the header. Sourced via `BackendApiClient.currentUserGithubToken()` (`private`), never logged. 401 from passthrough routes ≠ session expired — `AuthenticationStateImpl` debounces consecutive 401s under the same token before clearing the session.\n- **Platform branching:** Source sets are `commonMain` (shared), `androidMain` (Android), `jvmMain` (Desktop). Some features (apps, installation, Shizuku) are Android-only.\n- **Shizuku (Android):** Optional silent install via AIDL service. Falls back to standard installer on failure.\n\n## Coding Conventions\n\n- Packages: `zed.rainxch.{module}.{layer}` (e.g. `zed.rainxch.home.data.repository`)\n- Private state: underscore prefix `_state`, `_events`\n- Sealed classes/interfaces for type-safe routes, actions, events\n- Repository pattern: interface in `domain/`, implementation in `data/`\n- Ktlint auto-runs on `preBuild`/`compileKotlin*` tasks; `ignoreFailures = true`\n- Ktlint rules: wildcard imports allowed, filename rule disabled, `@Composable` functions exempt from function naming rule (see `.editorconfig`)\n\n## Adding a New Feature\n\n1. Create `feature/<name>/domain/`, `feature/<name>/data/`, `feature/<name>/presentation/`\n2. Add `build.gradle.kts` in each using the appropriate convention plugin\n3. Add `include` entries in `settings.gradle.kts`\n4. Define domain interfaces/models in `domain/`\n5. Implement repository + Koin DI module in `data/di/SharedModule.kt`\n6. Create ViewModel (State/Action/Event pattern) and Screen in `presentation/`\n7. Add navigation route to `GithubStoreGraph.kt` and wire in `AppNavigation.kt`\n8. Register the Koin module in `initKoin.kt`\n\n## Feature-Level Documentation\n\nEach `feature/` directory contains its own `CLAUDE.md` with module structure, key interfaces, navigation routes, and implementation notes. Read those for feature-specific guidance.\n\n## Versions\n\nAll library versions managed in `gradle/libs.versions.toml`. Key versions: Kotlin 2.3.10, Compose Multiplatform 1.10.3, Ktor 3.4.0, Room 2.8.4, Koin 4.1.1.\n","category":"root","tokens":1773},{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# Komi Store\n\nCross-platform app store for GitHub + Codeberg + Forgejo releases. **Kotlin Multiplatform** + **Compose Multiplatform**. Android (min API 26) + Desktop (JVM: Win/macOS/Linux). Package `zed.rainxch.githubstore`. Version 1.8.3 (code 18). Target SDK 36.\n\n## Build\n\n```bash\n./gradlew :composeApp:assembleDebug                                    # Android\n./gradlew :composeApp:run                                              # Desktop dev\n./gradlew :composeApp:packageExe :composeApp:packageMsi                # Win installer\n./gradlew :composeApp:packageDmg :composeApp:packagePkg                # macOS\n./gradlew :composeApp:packageDeb :composeApp:packageRpm                # Linux\n./gradlew build                                                        # full\n```\n\nJDK 21+. Android SDK for Android.\n\n## Structure\n\n```text\ncomposeApp/            # entry points, navigation, DI wiring (commonMain / androidMain / jvmMain)\ncore/\n  domain/              # interfaces, models, use cases (no framework deps)\n  data/                # repos, Ktor, Room, Koin, platform impls\n  presentation/        # Material 3 theme + reusable components + 14-locale strings\nfeature/\n  apps auth details dev-profile favourites homeP profile recently-viewed search starred tweaks\nbuild-logic/convention/  # convention plugins\n```\n\nEach feature: up to 3 sub-modules (`domain/`, `data/`, `presentation/`). `favourites`, `starred`, `recently-viewed` are presentation-only.\n\n## Architecture\n\nClean Architecture + MVVM. Layers: **Domain** (contracts), **Data** (Ktor + Room + Koin DI), **Presentation** (ViewModels with `StateFlow`/`Channel`, Compose).\n\n### State pattern (every screen)\n\n```kotlin\nclass XViewModel : ViewModel() {\n    private val _state = MutableStateFlow(XState())\n    val state = _state.asStateFlow()                  // or .stateIn(WhileSubscribed)\n    private val _events = Channel<XEvent>()\n    val events = _events.receiveAsFlow()\n    fun onAction(action: XAction) { ... }\n}\n```\n\n`State` = data class. `Action` = sealed (user input). `Event` = sealed (one-off effects).\n\n### Navigation\n\n`@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../app/navigation/`. Routes: `HomeScreen`, `SearchScreen`, `AuthenticationScreen`, `ProfileScreen`, `TweaksScreen`, `FavouritesScreen`, `StarredReposScreen`, `RecentlyViewedScreen`, `AppsScreen`, `OnboardingScreen`, `ExternalImportScreen`, `MirrorPickerScreen`, `StarredPickerScreen`, `SkippedUpdatesScreen`, `HiddenRepositoriesScreen`, `WhatsNewHistoryScreen`, `AnnouncementsScreen`, `HostTokensScreen`, `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate, sourceHost)`, `DeveloperProfileScreen(username)`. `DetailsScreen.sourceHost` is non-null for Codeberg / Forgejo / custom-forge repos — routes all `DetailsRepository` calls through `ForgejoClientRegistry` instead of the GitHub-backed default path.\n\n### DI\n\nKoin. Feature modules in `data/di/SharedModule.kt`. ViewModels in `composeApp/.../app/di/ViewModelsModule.kt` (`viewModelOf(::X)` or explicit `viewModel { ... }`). Wired in `initKoin.kt`.\n\n## Core repositories (`core/domain`)\n\n`FavouritesRepository`, `StarredRepository`, `InstalledAppsRepository`, `SeenReposRepository`, `HiddenReposRepository`, `SearchHistoryRepository`, `TweaksRepository`, `AuthenticationState`, `ThemesRepository`, `ProxyRepository`, `RateLimitRepository`, `ExternalImportRepository`, `TelemetryRepository`, `HostTokenRepository` (per-host PATs, KSafe-encrypted). Network: `ForgejoApiClient` + `ForgejoClientRegistry` (per-host Ktor clients, thread-safe via Mutex, proxy-aware, closes cached engines on shutdown / proxy change). Util: `AssetVariant` (token/glob/stem fingerprinting), `assetPlatformOf`, `RepoIdCodec` (23-bit host fingerprint + 40-bit raw id packed into the existing 64-bit `repoId` slot — sign bit = foreign source), `RepositoryUrlParser` (recognises GitHub + Codeberg + gitea.com + git.disroot.org + user-added forge hosts). System interfaces: `Installer`, `InstallerStatusProvider`, `PackageMonitor`, `SystemInstallSerializer`.\n\n## Tech\n\nKotlin 2.3.10, Compose Multiplatform 1.10.3, Ktor 3.4.0, Room 2.8.4, Koin 4.1.1, kotlinx.serialization 1.10.0, DataStore 1.2.0, Landscapist 2.9.5, Kermit 2.0.8, MOKO Permissions 0.20.1, Navigation Compose 2.9.2, multiplatform-markdown-renderer 0.39.2, Shizuku 13.1.5, WorkManager 2.11.1, kotlinx.datetime 0.7.1. Versions in `gradle/libs.versions.toml`.\n\n## Convention plugins (`build-logic/convention/`)\n\n`convention.kmp.library` (domain/data), `convention.cmp.library` (core/presentation), `convention.cmp.feature` (feature presentation), `convention.cmp.application` (main app), `convention.room`, `convention.buildkonfig`.\n\n## Adding a feature\n\n1. `feature/<name>/{domain,data,presentation}/` with appropriate convention plugin\n2. `include` in `settings.gradle.kts`\n3. Domain interfaces → impl + Koin module in `data/di/SharedModule.kt` → ViewModel + Screen\n4. Route in `GithubStoreGraph.kt` + wire in `AppNavigation.kt` + register Koin in `initKoin.kt`\n\n## Key configuration\n\n- **GitHub OAuth:** `GITHUB_CLIENT_ID` in `local.properties`. Deep links: `githubstore://auth` (web-OAuth handoff), `githubstore://callback` (legacy device-flow leftover), `githubstore://repo`, `githubstore://apps`.\n- **Shizuku (Android):** silent install via `ShizukuProvider` → AIDL → `pm install -S`. Fallback to standard installer on failure.\n- **Desktop logs:** `CrashReporter` (first line of `DesktopApp.main`) tees stdout/stderr to rotating `session.log` + writes `crash-<ts>.log` on uncaught. Paths: `~/Library/Logs/GitHub-Store/` (macOS), `%LOCALAPPDATA%/GitHub-Store/logs/` (Win), `$XDG_STATE_HOME/GitHub-Store/logs/` (Linux). Android = Logcat.\n- **macOS distribution:** Homebrew cask in tap `openhub-store/tap` (separate repo `homebrew-tap`). `brew install --cask github-store`. Unsigned at present — user must `xattr -dr com.apple.quarantine /Applications/GitHub-Store.app` after install. CI builds `.dmg` + `.pkg` on every push to `generate-installers`; tap cask updates automatically on release.\n- **`X-GitHub-Token` header:** Client attaches when `TokenStore.currentToken()` is non-null on `/v1/search`, `/v1/search/explore`, `/v1/repo`, `/v1/releases`, `/v1/readme`, `/v1/user`. Backend re-sends as `Authorization: token $token` to GitHub. Without it, backend round-robins a 4-token service pool. Upstream 401 remapped to backend `502` (handled like \"GitHub unreachable\" — fall back via `shouldFallbackToGithubOrRethrow`). `429` = no fallback (same wall), only backoff. `UnauthorizedInterceptor` only on direct-GitHub client; `AuthenticationStateImpl` debounces consecutive 401s by token snapshot.\n- **Auth flow (web-OAuth-first):** Primary path is web OAuth with PKCE + handoff. `feature/auth/data/crypto/PkceGenerator` mints `(state, codeVerifier, codeChallenge)`; `WebAuthApi.register` POSTs verifier + challenge + state to `https://github-store.org/auth/register` (Cloudflare Worker stashes them in Workers KV) and returns `authUrl`. User opens it, authorizes on `github.com`, GitHub redirects to `github-store.org/auth/callback?code&state` where the Worker exchanges the code via `api.github-store.org` (backend stores `(handoffId → access_token)` for 60s in Postgres with atomic `DELETE…RETURNING`), then bounces back to `githubstore://auth?h=<handoffId>`. App reads handoff via `WebAuthApi.consumeHandoff` (GETDEL semantics). Secondary path: device flow via backend `/v1/auth/device/start` + `/poll`, `AuthPath` (`Backend`|`Direct`) tracked in `SavedStateHandle`, only escalates `Backend → Direct` on infra errors. Tertiary: paste a Personal Access Token (`signInWithPat` — validates against `/user`, persists optimistically when GitHub unreachable). Backend rate limits: 10 device-starts/hr, 200 device-polls/hr per IP. Endpoints in `core/data/network/BackendEndpoints.kt` (`BACKEND_ORIGIN`, `WEB_ORIGIN`).\n- **Windows installer signing (SignPath Foundation):** CI workflow `.github/workflows/build-desktop-platforms.yml` job `sign-windows` after every push to `generate-installers` branch. Action pinned to commit SHA (not `@v2`). Secrets: `SIGNPATH_API_TOKEN`, `SIGNPATH_ORGANIZATION_ID` (`1ecf111e-...`). Variable `SIGNPATH_SIGNING_POLICY_SLUG` = `test-signing` until prod cert issued; flip to `release-signing`. Project slug `GitHub-Store`, artifact config slug `initial`. Unsigned artifact deleted post-sign; only `windows-installers-signed` reaches the draft release.\n- **WinGet publish:** `.github/workflows/winget-publish.yml` fires on `release: [released]`. Action `vedantmgoyal9/winget-releaser@main`. Secret `WINGET_TOKEN` = PAT with `Contents+Pull requests: write` on `OpenHub-Store/winget-pkgs` (fork of `microsoft/winget-pkgs`). Pin `fork-user: OpenHub-Store` explicitly so the action doesn't infer from token owner.\n- **Forges (Codeberg / Forgejo / Gitea):** `ForgejoApiClient` per host (60s req / 30s connect+socket timeouts, exponential retry on 5xx + IOException). `ForgejoClientRegistry.clientFor(host)` cached + Mutex-guarded. Direct-to-forge — no backend mediator. `RepoIdCodec` packs host fingerprint into `repoId` so the existing GitHub-shaped schema survives. README via `/contents/README.md?ref={branch}` (Forgejo has NO `/readme` endpoint). License sniffed from `/contents/LICENSE` regex against SPDX headers. Downloads aggregated by summing `asset.download_count` across releases.\n- **Per-host PATs:** `HostTokenRepository` stores `{host, token, label, createdAt}` rows AES-256-GCM encrypted via KSafe. `HostTokenInterceptor` (Ktor plugin) injects `Authorization: token $pat` on matched host. `HostNames.apiHostToTokenHost` maps `api.github.com → github.com` so the GitHub-direct client looks up the right PAT. UI at `Tweaks → Access Tokens` (`HostTokensScreen`).\n- **KSafe:** AES-256-GCM with hardware-backed Keystore on Android. Wraps every persisted credential / pref via `core/data/secure/KSafeSafe.kt` extension funcs (`safeGet`, `safePut`, `safeDelete`, `safeGetFlow`) — surface log + return null/false on transient failure instead of throwing through coroutine scopes.\n- **Translation providers:** `TranslationProvider` enum = `GOOGLE`, `YOUDAO`, `LIBRE_TRANSLATE`, `DEEPL`, `MICROSOFT`. Each per-provider config persisted via `TweaksRepository` (KSafe-encrypted). `TranslationRepositoryImpl.resolveTranslator()` picks the impl. LibreTranslate defaults to the bundled `translate.disroot.org` mirror when user URL pref blank. DeepL auto-routes `:fx`-suffixed keys to `api-free.deepl.com`. Microsoft uses No-Trace by default — text never stored, never used for training.\n- **Gradle:** Config + build cache enabled. 4GB Gradle heap, 3GB Kotlin daemon. Official Kotlin style.\n\n## Active skills (apply on matching domain)\n\n- **caveman** — session default, terse output.\n- **karpathy-guidelines** — anti-overcomplication, minimal diffs, surface assumptions, verifiable success criteria. Every coding task.\n- **one-skill-to-rule-them-all** — watch for skill-capture opportunities during multi-step work.\n- **gsd-inbox** - Triage open GitHub issues + PRs against templates. Our exact pattern — automate the \"check issue #N, draft reply, ship fix\" loop.\n- **gsd-ship** - Create PR + review + prep for merge. Every task ends here.\n- **gsd-quick** - Trivial task with atomic commits + state tracking. Matches our small-commit policy.\n- **gsd-debug** - Systematic debugging with persistent state across context resets. For bug-hunt cycles.\n- **android-* skills** (`~/.claude/skills/android/`) — auto-fire by description match; apply when in matching domain:\n  - `android-compose-ui` — composables, recomposition, animations, modifiers, design system\n  - `android-data-layer` — repos, DTOs, Room, Ktor, mappers\n  - `android-di-koin` — Koin module setup, ViewModel injection\n  - `android-error-handling` — Result wrapper, typed errors\n  - `android-module-structure` — feature-layered modules, convention plugins\n  - `android-navigation` — type-safe Compose nav\n  - `android-presentation-mvi` — State/Action/Event, Root/Screen split, UiText, SavedStateHandle\n  - `android-testing` — testing patterns\n\n## Conventions\n\n- Packages `zed.rainxch.{module}.{layer}`\n- Private state fields prefix `_state`\n- Sealed routes/actions/events\n- Repository pattern: interface in `domain/`, impl in `data/`\n- Source sets: `commonMain` shared, `androidMain`, `jvmMain`\n- **No KDoc, no inline comments** unless the user explicitly asks. No function/class docs. Inline only for non-obvious invariants, tricky concurrency, workarounds. Applies globally.\n- Feature-specific guidance in each `feature/*/CLAUDE.md`\n\n## Approach\n\n- Read existing files before writing. Don't re-read unless changed.\n- Thorough in reasoning, concise in output.\n- Skip files over 100KB unless required.\n- No sycophantic openers or closing fluff.\n- No emojis or em-dashes.\n- Do not guess APIs, versions, flags, commit SHAs, or package names. Verify by reading code or docs before asserting, researching if necessary.\n","category":"root","tokens":3240}]}