import { execFile } from "node:child_process"; import { chmod, mkdir, writeFile } from "node:fs/promises"; import { createRequire } from "node:module"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; import { dbg, diagnosticKind } from "../debug.js"; import { findExecutable } from "../pi-ext-executable.ts"; const execFileAsync = promisify(execFile); const require = createRequire(import.meta.url); const PACKAGE = "@anthropic-ai/claude-agent-sdk-linux-arm64"; let resolution: Promise | undefined; function shellQuote(value: string): string { return `'${value.replaceAll("'", `'"'"'`)}'`; } // Android is bionic, so the glibc arm64 claude binary runs through glibc-runner // (grun). The binary itself must already be installed (npm skips the SDK's // `os: linux` packages on an `android` platform, so it is not auto-resolved); // this runtime only locates it and writes a Termux-safe grun launcher. async function prepare(): Promise { dbg?.("runtime.android.prepare.start"); if (process.arch !== "arm64") { throw new Error(`Klaus does not support Android ${process.arch}.`); } const grun = findExecutable(["grun"]); if (!grun) { throw new Error( "Klaus requires glibc-runner on Android: pkg install glibc-repo && pkg install glibc-runner", ); } let packageJson: string; try { packageJson = require.resolve(`${PACKAGE}/package.json`); } catch { throw new Error( `Klaus runtime ${PACKAGE} is not installed; install it alongside klaus so grun can launch it.`, ); } const claude = join(dirname(packageJson), "claude"); const root = join( process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"), "pi", "klaus", "runtime", ); await mkdir(root, { recursive: true, mode: 0o700 }); const launcher = join(root, "claude-klaus"); await writeFile( launcher, `#!/data/data/com.termux/files/usr/bin/sh\nexec ${shellQuote(grun)} ${shellQuote(claude)} "$@"\n`, { mode: 0o700 }, ); await chmod(launcher, 0o700); await execFileAsync(launcher, ["--version"], { timeout: 30_000, maxBuffer: 64 * 1024, }); return launcher; } export function resolveAndroidRuntime(): Promise { dbg?.("runtime.android.resolve", { cached: Boolean(resolution) }); resolution ??= prepare().catch((error) => { dbg?.("runtime.android.resolve.error", { kind: diagnosticKind(error) }); resolution = undefined; throw error; }); return resolution; }