Ketoy
Getting started

Installation

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


Requirements

ToolVersion
Android Gradle Plugin8.13.x
Kotlin2.0.21
JDK17
Jetpack Compose BOM2024.10.00
minSdk26
compileSdk35 or higher

Ketoy 0.3.x's compiler plugin is pinned to this specific Kotlin / AGP / Compose combination. (ADR-0004 will ship Ketoy's own embedded Kotlin compiler so the version pin can be lifted — until then, match the table above.)


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.3.4-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.3.4-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.3.4-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 KetoyVM / KetoyBundleLoader log lines.

Next: Write your first screen →