agentsclimarketplace

Main kts

Skill Heapy/kortex/plugins/kortex/skills/main-kts

Agent Skills for Kotlin Development

Install
npx -y skills add Heapy/kortex --skill main-kts

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when creating, writing, or running standalone executable Kotlin scripts with the `.main.kts` extension, including the `kotlin` script runner, shebang execution, `@file:DependsOn`/`@file:Repository`/`@file:Import`/`@file:CompilerOptions`/`@file:OptIn` annotations, script dependencies, command-line `args`, compiled-script caching, and common scripting libraries such as ktor, kotlinx-serialization, kotlinx-coroutines, clikt, and kotlinx-html.

SKILL.md

19.2 KB, as published. Nobody here has run it

Kotlin Scripts (.main.kts)

Use this skill to generate and run executable Kotlin .main.kts scripts: single-file programs that declare their own dependencies, run directly from the shell, and need no build file.

This is the kotlin script runner, not Kotlin Toolchain, Gradle, or Maven. Do not add module.yaml, build.gradle, or pom.xml. If the task needs a real multi-file project or published artifact, it is not a .main.kts task.

When To Use

  • A one-off automation, glue, or CLI utility that should run with ./script.main.kts or kotlin script.main.kts.
  • The user wants Kotlin without project scaffolding or a build system.
  • Quick API calls, file munging, JSON processing, HTML generation, or shell orchestration.

Prefer a real Kotlin Toolchain or Gradle module when the code grows multiple source files, needs incremental builds, or ships as a library or application artifact.

Gather Requirements

Before writing, confirm:

  1. What the script should do (purpose and behavior).
  2. A name (suggest one from the behavior; the file must end in .main.kts).
  3. Where to save it (default: current directory).
  4. Any inputs it takes (command-line args, env vars, stdin).

Script Structure

@file: annotations must come first, before any import or code. Order them: Repository, DependsOn, Import, CompilerOptions, OptIn.

#!/usr/bin/env kotlin

/**
 * [What the script does.]
 *
 * Usage:
 *   ./script.main.kts <args>
 */

// Custom repositories. If any @file:Repository is present, list every repository needed.
// Maven Central is not kept implicitly once custom repositories are declared.
@file:Repository("https://repo.maven.apache.org/maven2/")
@file:Repository("https://jitpack.io")

// Maven dependencies
@file:DependsOn("group:artifact:version")

// Compose other scripts (optional)
@file:Import("other.main.kts")

// Compiler flags (optional)
@file:CompilerOptions("-jvm-target", "21")

// Opt-ins (optional)
@file:OptIn(ExperimentalStdlibApi::class)

import ...

// Top-level script code runs directly; `args` is available.

Key Mechanics

  • Extension: use .main.kts when the script needs the main.kts script definition, especially @file:DependsOn, @file:Repository, @file:Import, or compiler options. A plain .kts script run with kotlin script.kts arg1 can still read args; do not rename solely for CLI arguments.
  • Shebang: #!/usr/bin/env kotlin enables ./script.main.kts. After writing, suggest chmod +x script.main.kts.
  • Run: kotlin script.main.kts arg1 arg2 or, once executable, ./script.main.kts arg1 arg2.
  • Runner rename (Kotlin 2.4.10+): starting with Kotlin 2.4.10 the script runner is kotlinr, not kotlin. On those versions run kotlinr script.main.kts and use #!/usr/bin/env kotlinr in the shebang. Earlier versions keep using kotlin.
  • Args: command-line arguments arrive as the args: Array<String> variable. Validate and fail fast with error(...) or kotlin.system.exitProcess(1).
  • Dependencies: @file:DependsOn("group:artifact:version") resolves from Maven Central by default when no @file:Repository annotations are present. If you add any repository annotation, list every repository the script needs, including Maven Central (https://repo.maven.apache.org/maven2/) plus custom repositories such as Google or JitPack. Version is required; do not rely on ranges.
  • Composition: @file:Import("other.main.kts") inlines another script so it can share top-level declarations.
  • Caching: compiled scripts are cached and recompiled only when the source or its @file: annotations change. The cache lives at $KOTLIN_MAIN_KTS_COMPILED_SCRIPTS_CACHE_DIR, or ~/Library/Caches/main.kts.compiled.cache on macOS by default. Disable by setting that variable to an empty value if you need a clean compile.
  • The cache key ignores command-line compiler flags. It hashes the source and its @file: annotations only — not -Xplugin or other flags passed to the runner. So toggling -Xplugin on an unchanged file does not force a recompile: a run with the serialization plugin caches serializer-bearing bytecode that a later run without the plugin silently reuses (and vice versa). When testing whether a compiler plugin actually applies, clear the cache first — otherwise a stale entry masks the real result. This is a genuine debugging trap: an @Serializable script can appear to work with no plugin simply because an earlier plugged-in run populated the cache.
  • First run is slow: dependency resolution and compilation happen on the first run; later runs hit the cache.

Running: kotlin vs kotlinc -script

Both compile and execute a .main.kts — since Kotlin 1.3.70 the runner handles scripts "the same way as kotlinc -script". They differ in the interface, how script arguments are passed, and how you supply compiler options:

kotlin script.main.kts args...kotlinc -script script.main.kts -- args...
What it isRunner wrapper - detects the script, compiles, runs. Best for CLI/shebang.Compiler CLI - you state explicitly that it is a script.
Script argsEverything after the file name becomes args.Use -- to split script args from compiler options.
Compiler pluginkotlin -Xplugin=... script.main.ktskotlinc -script -Xplugin=... script.main.kts

Both apply -Xplugin at the script's compile phase — this is how @Serializable is made to work (see Serialization formats). For debugging compiler-plugin problems, prefer kotlinc -script: you work with the compiler and its options directly, without the runner's own command-line parsing in between.

Common Dependencies

LibraryCoordinateUse case
ktor-clientio.ktor:ktor-client-cio-jvm:3.5.1HTTP client (async; see below)
kotlinx-coroutinesorg.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0async / concurrency
cliktcom.github.ajalt.clikt:clikt-jvm:5.1.0CLI argument parsing
kotlinx-htmlorg.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0HTML generation

Versions move; confirm the latest stable coordinate on central.sonatype.com before pinning if the script must stay current.

Use the -jvm artifact for Kotlin Multiplatform libraries. @file:DependsOn resolves through a Maven resolver that reads .pom files and ignores Gradle Module Metadata (the .module files). Most KMP libraries here (Ktor, clikt, and the format libraries below) publish their bare coordinate as an empty umbrella jar with no JVM classes — the bytecode lives in the -jvm platform artifact — so the bare coordinate resolves but yields unresolved reference at compile time. Depend on io.ktor:ktor-client-cio-jvm, not io.ktor:ktor-client-cio. The -jvm leaf always works, so it is the one uniform rule; a library already pinned to a -jvm (or plain-JVM) leaf — like kotlinx-html-jvm — needs no change.

The exception proves the mechanism: kotlinx-serialization and kotlinx-coroutines publish their root as a POM that declares a compile dependency on their own -jvm artifact, which the resolver follows — so kotlinx-serialization-json (no -jvm) also resolves. It is not that these libraries are special to the runner; it is that they redirect via POM where the umbrella libraries do not. Verified on Kotlin 2.4.0 by inspecting each root POM and jar; -jvm is still the safe uniform choice.

Serialization formats

These build on kotlinx.serialization — in a real module you annotate a data class with @Serializable once and swap the format backend (JSON, YAML, TOML, XML, CSV) over the same model.

@Serializable needs the serialization compiler plugin, passed on the command line. A bare kotlin script.main.kts does not apply it, and @file:CompilerOptions("-Xplugin=…") does not enable it either — so encode/decode throws Serializer for class … is not found at runtime. It does work when you pass the plugin as a -Xplugin flag to the runner or compiler (verified on Kotlin 2.4.0): kotlin -Xplugin="$KOTLIN_HOME/lib/kotlinx-serialization-compiler-plugin.jar" script.main.kts. To keep ./script.main.kts self-contained via a shebang, bake that flag in with a shebang form below (env -S or a shell polyglot); or skip codegen and walk the parsed tree (JSON shown below). When toggling the plugin on and off to check whether it applied, clear the compiled-script cache between runs — it is not keyed by -Xplugin, so a stale entry will mask the result (see Caching).

FormatCoordinate (-jvm leaf, see the KMP note above)
JSONorg.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0
YAMLio.heapy.kotaml:kotaml-jvm:0.110.0
TOMLcom.akuleshov7:ktoml-core-jvm:0.7.1
XMLio.github.pdvrieze.xmlutil:serialization-jvm:1.0.0
CSVapp.softwork:kotlinx-serialization-csv-jvm:0.0.23

JSON in a script works without the plugin if you skip @Serializable and walk the parsed tree instead:

@file:DependsOn("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0")

import kotlinx.serialization.json.*

val root = Json.parseToJsonElement(jsonText)          // no @Serializable, no compiler plugin
val name = root.jsonObject["name"]!!.jsonPrimitive.content

Self-contained shebang version. Passing -Xplugin on the command line works, but then ./script.main.kts no longer runs standalone. To bake the plugin into the file so it executes directly, use one of two shebang forms. Both need KOTLIN_HOME to point at the compiler, and both must be run as ./script.main.kts, not kotlin script.main.kts — go through the runner and the shebang is never read, so -Xplugin is skipped and @Serializable fails at runtime again.

Option A — env -S (needs GNU coreutils 8.30+/2018 or BSD env). env -S splits the shebang string and hands the runner kotlin -Xplugin=… script.main.kts args… — exactly the argv documented above. But ${VAR} expansion inside a real shebang is platform-split, and the difference decides whether the file runs at all:

#!/usr/bin/env -S kotlin -Xplugin=${KOTLIN_HOME}/lib/kotlinx-serialization-compiler-plugin.jar
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable data class Data(val a: Int, val b: String)
println(Json.encodeToString(Data(42, "str")))         // prints {"a":42,"b":"str"}
  • On Linux ${KOTLIN_HOME} expands; on macOS it does not — the kernel decides, not env. Linux hands env the whole shebang tail as one argument, so env -S splits and expands it. macOS/XNU splits the #! line on whitespace before env runs, so env -S only sees the first token as its -S operand; ${KOTLIN_HOME} arrives as a separate, already-split argv element that env passes through verbatim. The literal string reaches the compiler, which dies with plugin classpath entry points to a non-existent location: ${KOTLIN_HOME}/…. Tested on macOS + Kotlin 2.4.0 (fails); env -S expands the variable only when called from a shell, not from a shebang. The Linux path is inferred from its single-arg shebang contract, not run in this session.
  • Where expansion does happen, braces are mandatory. ${KOTLIN_HOME} expands; $KOTLIN_HOME fails with only ${VARNAME} expansion is supported-S's own parser, not the shell.
  • On macOS, hardcode the absolute jar path (-Xplugin=/…/lib/kotlinx-serialization-compiler-plugin.jar) — that form is verified working. The cost is length: the kernel measures the shebang against BINPRM_BUF_SIZE (128 bytes before Linux 5.1, 256 after), and a full jar path runs ~126 bytes even from a short prefix — brushing the old cap. The variable was what kept the line short, and that is exactly what macOS won't expand, so there is no headroom to gain here.
  • On Kotlin 2.4.10+ the runner is kotlinr, so the shebang becomes #!/usr/bin/env -S kotlinr -Xplugin=….

Option B — shell/Kotlin polyglot (portable; bare POSIX sh). Uglier, but needs no env -S, so it runs on any POSIX shell:

#!/bin/sh
//usr/bin/env kotlinc -script -Xplugin="$KOTLIN_HOME/lib/kotlinx-serialization-compiler-plugin.jar" -- "$0" "$@"; exit $?
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable data class Data(val a: Int, val b: String)
println(Json.encodeToString(Data(42, "str")))         // prints {"a":42,"b":"str"}

Run it as ./script.main.kts (or sh script.main.kts). Line 2 is the pivot: the shell reads //usr/bin/env… as the path /usr/bin/env and execs the compiler, while Kotlin reads the same line as a // comment. Keep that exec on a single // line — a /* written after // stays inside the line comment and never opens a block comment, so the shell lines leak into Kotlin and fail to compile (the common trap).

Both examples use the plain kotlinx-serialization-json coordinate because its root POM redirects to -jvm (see the KMP note); an umbrella-only library would still need its -jvm leaf here. Because Option B expands $KOTLIN_HOME in a real shell line, it keeps a variable path working everywhere, macOS included — so it is the portable choice for a single file that must run on both. Use Option A's variable form on Linux; on macOS use Option B or hardcode the jar path. Even so, a real Gradle/Kotlin module is the clean answer; use either shebang only for a genuinely single-file need.

Ktor client modules

Each engine pulls in ktor-client-core-jvm transitively, so depend on one engine alone — pick it:

EngineNotes
io.ktor:ktor-client-cio-jvm:3.5.1Pure-Kotlin coroutine engine, no extra deps. Good default.
io.ktor:ktor-client-java-jvm:3.5.1JDK 11+ java.net.http; HTTP/2, no third-party deps.
io.ktor:ktor-client-okhttp-jvm:3.5.1OkHttp-backed; HTTP/2, connection pooling.
io.ktor:ktor-client-apache5-jvm:3.5.1Apache HttpClient 5.

Plugins are separate artifacts; add them alongside the engine as needed:

PluginPurpose
io.ktor:ktor-client-content-negotiation-jvm:3.5.1(De)serialize bodies by Content-Type.
io.ktor:ktor-client-logging-jvm:3.5.1Request/response logging.
io.ktor:ktor-client-auth-jvm:3.5.1Basic / Bearer (with token refresh) / Digest auth.
io.ktor:ktor-client-encoding-jvm:3.5.1gzip/deflate compression handling.
io.ktor:ktor-client-websockets-jvm:3.5.1WebSocket support.

Content negotiation also needs a converter — io.ktor:ktor-serialization-kotlinx-json-jvm:3.5.1 (note the ktor-serialization-* prefix, not ktor-client-*). It relies on @Serializable, so the codegen caveat above applies in a plain script.

Shelling out

Use the JDK's ProcessBuilder — it is enough for scripts and needs no dependency.

After Generation

  1. Write the file with the .main.kts extension.
  2. Tell the user to run chmod +x <name>.main.kts if they want ./<name>.main.kts execution.
  3. Show the exact run command and what output to expect.
  4. Note the first run compiles and downloads dependencies, so it is slower than later runs.

Example: Fetch A URL With Ktor

#!/usr/bin/env kotlin

/**
 * Fetches a URL and prints the response body.
 *
 * Usage:
 *   ./fetch.main.kts https://api.example.com/data
 */

@file:DependsOn("io.ktor:ktor-client-cio-jvm:3.5.1")

import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.isSuccess
import kotlinx.coroutines.runBlocking
import kotlin.system.exitProcess

val url = args.firstOrNull() ?: run {
    System.err.println("Usage: fetch.main.kts <url>")
    exitProcess(1)
}

runBlocking {
    HttpClient(CIO).use { client ->
        val response = client.get(url)
        if (!response.status.isSuccess()) {
            System.err.println("HTTP ${response.status}")
            exitProcess(1)
        }
        println(response.bodyAsText())
    }
}

Pitfalls

  • A .kts file that is not .main.kts can still see args, but it will not resolve main.kts annotations such as @file:DependsOn; rename only when those script-definition features are needed.
  • @file: annotations after the first import or statement are ignored or fail — keep them at the top.
  • Missing versions in @file:DependsOn break resolution; always pin group:artifact:version.
  • Editing a script invalidates its cache and forces a recompile; do not expect instant runs after every change.
  • For dependencies on JitPack, Google, or private repos, add @file:Repository(...) before @file:DependsOn(...) and include Maven Central explicitly too if any dependency still comes from Central.
  • Kotlin Multiplatform deps resolve to an empty umbrella unless you use the -jvm artifact — unresolved reference despite a correct @file:DependsOn usually means a missing -jvm suffix (see Common Dependencies).
  • @Serializable codegen is off by default in .main.kts (the plugin is not applied), so it fails at runtime, not compile time. Enable it by passing -Xplugin=…serialization… to the runner/compiler, parse JSON via JsonElement, or move the logic to a real module.
  • const val is rejected at the top level of a script — use a plain val (top-level script declarations are not inside an object).
  • Long-lived or multi-file logic belongs in a real module, not a script.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.