Ketoy
Getting started

Installation

This page walks you through adding Ketoy 0.4.16-alpha to an Android project — either via the **ketoy CLI** (fastest path, recommended for new projects) or by editing Gradle files yourself.


Requirements

As of 0.4.16-alpha, Ketoy no longer pins your project to a single Kotlin / AGP / Gradle / Compose combination. The compiler plugin ships its own embedded Kotlin compiler (ADR-0004) that runs in an isolated worker, and the Compose version is decoupled for the supported parameter surface via name-based remapping (ADR-0005). You bump these axes freely, only the floors matter:

ToolMinimumNotes
Kotlin2.0.0Any newer version (2.1, 2.2, 2.3) works, your toolchain never loads Ketoy's pinned compiler.
Android Gradle Pluginany supporting your compileSdkVerified up to AGP 9.1.1. See the AGP-9 plugin caveat below.
Gradle8.6Needs the Problems API for build-time advisories.
Jetpack Compose BOManyCatalog generated against BOM 2024.11.00 (Material3 1.3.x); newer/older render correctly for every catalogued param.
JDK17
minSdk26
compileSdk35 or higher

Ketoy pins Kotlin 2.0.21 internally for KBC emission, that governs only what Kotlin syntax you can write inside a @KetoyComposable body (the KBC subset), not what version your app uses. For the full picture of what being on a newer or older version means in both directions, see the Version compatibility update.

AGP-9 plugin caveat

AGP 9 ships built-in Kotlin and removed the legacy BaseExtension that the standalone org.jetbrains.kotlin.android plugin casts to. Under AGP 9 you must not apply org.jetbrains.kotlin.android, keep com.android.application

  • org.jetbrains.kotlin.plugin.compose. This is a general AGP-9 migration step independent of Ketoy; the dev.ketoy.compiler plugin is unaffected.

The CLI scaffolds everything for you with diff-and-confirm on every file edit. It's an AI-powered agent, but the init command runs deterministic template edits, no LLM in the loop.

Install

bash
npm install -g ketoy-dev      # binary is `ketoy`; npm package is `ketoy-dev`
ketoy version

Requires Node.js ≥ 20.

Authenticate (only needed for chat / migrate / doctor)

bash
ketoy auth anthropic           # paste your API key when prompted
# or: openai | google | mistral | groq | xai | openrouter | ollama

Credentials are stored at ~/.ketoy-cli/config.json with mode 0600. The CLI refuses to print them back.

Initialize a project

From inside an Android project root (the folder with settings.gradle.kts and app/build.gradle.kts):

bash
ketoy init

The CLI plans every edit, shows a unified diff, asks for confirmation, then:

  1. Inserts id("dev.ketoy.compiler") version "0.4.16-alpha" into your app module's plugins block.
  2. Appends the Ketoy dependencies (BOM + runtime + capabilities + adapters).
  3. Appends a ketoy { exportFromAppModule = true; bundleId = "main"; bundleVariant = "release"; … } extension block.
  4. Creates MyApplication.kt (or merges into your existing Application class) with the runtime bootstrap.
  5. Wraps MainActivity's setContent { … } body with a KetoyScreen { /* native fallback */ } (you can opt out with --no-install-screen).
  6. Creates HelloKetoyScreen.kt, a starter @KetoyEntryPoint @KetoyComposable.
  7. Creates app/ketoy-capabilities.json (empty, fill in as you add custom capabilities).
  8. Appends **/keys/*-private.key to .gitignore.

Then:

bash
./gradlew :app:assembleDebug

You've got a real .ktx bundle running inside your APK.

CLI cheat sheet

ketoy init [--install-screen | --no-install-screen] [--hilt | --no-hilt]
ketoy chat                                  # interactive AI agent
ketoy migrate <file>                        # AI-driven Compose → KBC port
ketoy doctor [task]                         # diagnose Gradle errors
ketoy build [--variant bundle|debug|release]
ketoy analyze <path.ktx> [--manifest] [--strings] [--json]
ketoy config list | get | set
ketoy auth [provider] [--remove] [--list]
ketoy version

Path 2, Manual setup

If you'd rather wire Ketoy by hand, here's the full setup.

1. Top-level settings.gradle.kts

kotlin
pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

2. App module build.gradle.kts

kotlin
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
    id("dev.ketoy.compiler") version "0.4.16-alpha"   // ← Ketoy plugin
    id("kotlin-kapt")                                  // if using Hilt
    id("dagger.hilt.android.plugin")                   // if using Hilt
}

android {
    namespace = "com.example.myapp"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.example.myapp"
        minSdk = 26
        targetSdk = 35
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions { jvmTarget = "17" }

    buildFeatures { compose = true }
}

dependencies {
    val ketoyBom = platform("dev.ketoy.vm:ketoy-bom:0.4.16-alpha")
    implementation(ketoyBom)

    implementation("dev.ketoy.vm:ketoy-runtime")
    implementation("dev.ketoy.vm:ketoy-annotations")
    implementation("dev.ketoy.vm:ketoy-capabilities-core")
    implementation("dev.ketoy.vm:ketoy-capabilities-navigation")
    implementation("dev.ketoy.vm:ketoy-adapters-material3")

    // Optional, Hilt integration
    implementation("dev.ketoy.vm:ketoy-hilt")

    // Standard Compose deps
    implementation(platform("androidx.compose:compose-bom:2024.10.00"))
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui")
    implementation("androidx.activity:activity-compose:1.9.3")

    // Test
    testImplementation("dev.ketoy.vm:ketoy-test")
}

// Ketoy compiler-plugin configuration
ketoy {
    exportFromAppModule.set(true)            // emit .ktx into THIS app module
    bundleId.set("main")                      // produces main.ktx
    bundleVariant.set("release")              // attach plugin to release variant
    capabilityRegistryFile.set(
        file("ketoy-capabilities.json")
    )
    signingKeyFile.set(
        file("keys/main-private.key")        // 32-byte Ed25519 seed
    )
    minAppVersion.set(0)                      // gates bundle activation per app version
}

3. Generate an Ed25519 key pair

bash
mkdir -p app/keys app/src/main/assets/ketoy/keys

# Generate private key (32-byte raw seed)
openssl genpkey -algorithm Ed25519 -outform DER -out /tmp/ed25519.der
tail -c 32 /tmp/ed25519.der > app/keys/main-private.key

# Derive public key (32 bytes, the trust anchor shipped in the APK)
openssl pkey -in /tmp/ed25519.der -inform DER -pubout -outform DER \
  | tail -c 32 > app/src/main/assets/ketoy/keys/main-public.key

Then add to .gitignore:

**/keys/*-private.key

4. app/ketoy-capabilities.json

Start empty, populate as you wire custom capabilities (see Custom Capability):

json
{
  "version": 1,
  "allowedStdlibFqNames": [],
  "capabilities": []
}

5. Bootstrap the runtime

Create MyApplication.kt:

kotlin
package com.example.myapp

import android.app.Application
import dev.ketoy.adapters.material3.registerGeneratedAdapters
import dev.ketoy.adapters.material3.registerGeneratedConstructors
import dev.ketoy.runtime.KetoyConfig
import dev.ketoy.runtime.KetoyRuntime
import dev.ketoy.runtime.bundle.KetoyBundleLoader
import dev.ketoy.runtime.capability.CapabilityRegistry
import dev.ketoy.runtime.security.KetoyKeystore

class MyApplication : Application() {
    lateinit var runtime: KetoyRuntime
        private set
    lateinit var bundleLoader: KetoyBundleLoader
        private set

    override fun onCreate() {
        super.onCreate()

        val publicKey = KetoyKeystore.loadFromAsset(
            this, "ketoy/keys/main-public.key"
        )

        val capabilities = CapabilityRegistry()
        // capabilities.registerCoreCapabilities(this), see Networking / DataStore guides

        runtime = KetoyRuntime(
            capabilityRegistry = capabilities,
            config = KetoyConfig(
                enableSignatureVerification = true,
                publicKey = publicKey,
                enableJIT = true,
                dexCacheDir = codeCacheDir,
            )
        )

        runtime.adapterRegistry.registerGeneratedAdapters(runtime)
        runtime.constructorRegistry.registerGeneratedConstructors()

        bundleLoader = KetoyBundleLoader(runtime, this)
    }
}

Register it in AndroidManifest.xml:

xml
<application android:name=".MyApplication" ...>

6. Wire into MainActivity.kt

kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val app = application as MyApplication

        setContent {
            MyAppTheme {
                CompositionLocalProvider(
                    LocalKetoyCapabilityRegistry provides app.runtime.capabilityRegistry,
                    LocalKetoyBundleLoader provides app.bundleLoader,
                ) {
                    KetoyScreen(
                        bundleSource = KetoyBundleSource.Asset("ketoy/main.ktx"),
                        entryPoint = "HelloKetoyScreen",
                        nativeFallback = { HelloNativeFallback() }
                    )
                }
            }
        }
    }
}

@Composable
private fun HelloNativeFallback() {
    Surface { Text("Ketoy bundle not available, using native fallback.") }
}

Verify the install

bash
./gradlew :app:ketoyBundle           # produces app/src/main/assets/ketoy/main.ktx
./gradlew :app:assembleDebug         # builds the APK
./gradlew :app:installDebug          # installs onto a connected device

You should see the HelloKetoyScreen content on launch. If you instead see the native fallback, the bundle didn't load, check adb logcat for Ketoy / KetoyBundleLoader log lines.

Next: Write your first screen →