import yargs from "yargs" import { hideBin } from "yargs/helpers" import { RunCommand } from "./cli/cmd/run" import { GenerateCommand } from "./cli/cmd/generate" import { Log } from "./util/log" import { AuthCommand } from "./cli/cmd/auth" import { AgentCommand } from "./cli/cmd/agent" import { UpgradeCommand } from "./cli/cmd/upgrade" import { UninstallCommand } from "./cli/cmd/uninstall" import { ModelsCommand } from "./cli/cmd/models" import { UI } from "./cli/ui" import { Installation } from "./installation" import { NamedError } from "@opencode-ai/util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" import { McpCommand } from "./cli/cmd/mcp" // import { GithubCommand } from "./cli/cmd/github" // kilocode_change import { ExportCommand } from "./cli/cmd/export" import { ImportCommand } from "./cli/cmd/import" import { AttachCommand } from "./cli/cmd/tui/attach" import { TuiThreadCommand } from "./cli/cmd/tui/thread" import { AcpCommand } from "./cli/cmd/acp" import { EOL } from "os" import { WebCommand } from "./cli/cmd/web" import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" // kilocode_change start - Import telemetry, instance disposal, and legacy migration import { Telemetry } from "@kilocode/kilo-telemetry" import { Instance } from "./project/instance" // kilocode_change import { migrateLegacyKiloAuth, ENV_FEATURE } from "@kilocode/kilo-gateway" // kilocode_change - set feature for tracking. 'serve' is spawned by other services // (extension, cloud) which set their own KILOCODE_FEATURE env var. Direct CLI use // (any command other than 'serve') is tagged as 'cli'. If 'serve' is spawned without // the env var, it gets 'unknown' so the misconfiguration is visible in data. if (!process.env[ENV_FEATURE]) { const isServe = process.argv.includes("serve") process.env[ENV_FEATURE] = isServe ? "unknown" : "cli" } import { Config } from "./config/config" import { Auth } from "./auth" // kilocode_change end import { DbCommand } from "./cli/cmd/db" import path from "path" import { Global } from "./global" import { JsonMigration } from "./storage/json-migration" import { Database } from "./storage/db" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { e: e instanceof Error ? e.message : e, }) }) process.on("uncaughtException", (e) => { Log.Default.error("exception", { e: e instanceof Error ? e.message : e, }) }) const cli = yargs(hideBin(process.argv)) .parserConfiguration({ "populate--": true }) .scriptName("kilo") // kilocode_change .wrap(100) .help("help", "show help") .alias("help", "h") .version("version", "show version number", Installation.VERSION) .alias("version", "v") .option("print-logs", { describe: "print logs to stderr", type: "boolean", }) .option("log-level", { describe: "log level", type: "string", choices: ["DEBUG", "INFO", "WARN", "ERROR"], }) .middleware(async (opts) => { await Log.init({ print: process.argv.includes("--print-logs"), dev: Installation.isLocal(), level: (() => { if (opts.logLevel) return opts.logLevel as Log.Level if (Installation.isLocal()) return "DEBUG" return "INFO" })(), }) process.env.AGENT = "1" process.env.OPENCODE = "1" Log.Default.info("opencode", { version: Installation.VERSION, args: process.argv.slice(2), }) // kilocode_change start - Initialize telemetry const globalCfg = await Config.getGlobal() await Telemetry.init({ dataPath: Global.Path.data, version: Installation.VERSION, enabled: globalCfg.experimental?.openTelemetry !== false, }) // Migrate legacy Kilo CLI auth if needed await migrateLegacyKiloAuth( async () => (await Auth.get("kilo")) !== undefined, async (auth) => Auth.set("kilo", auth), ) const kiloAuth = await Auth.get("kilo") if (kiloAuth) { const token = kiloAuth.type === "oauth" ? kiloAuth.access : kiloAuth.key const accountId = kiloAuth.type === "oauth" ? kiloAuth.accountId : undefined await Telemetry.updateIdentity(token, accountId) } Telemetry.trackCliStart() // kilocode_change end const marker = path.join(Global.Path.data, "kilo.db") if (!(await Bun.file(marker).exists())) { const tty = process.stderr.isTTY process.stderr.write("Performing one time database migration, may take a few minutes..." + EOL) const width = 36 const orange = "\x1b[38;5;214m" const muted = "\x1b[0;2m" const reset = "\x1b[0m" let last = -1 if (tty) process.stderr.write("\x1b[?25l") try { await JsonMigration.run(Database.Client().$client, { progress: (event) => { const percent = Math.floor((event.current / event.total) * 100) if (percent === last && event.current !== event.total) return last = percent if (tty) { const fill = Math.round((percent / 100) * width) const bar = `${"■".repeat(fill)}${"・".repeat(width - fill)}` process.stderr.write( `\r${orange}${bar} ${percent.toString().padStart(3)}%${reset} ${muted}${event.label.padEnd(12)} ${event.current}/${event.total}${reset}`, ) if (event.current === event.total) process.stderr.write("\n") } else { process.stderr.write(`sqlite-migration:${percent}${EOL}`) } }, }) } finally { if (tty) process.stderr.write("\x1b[?25h") else { process.stderr.write(`sqlite-migration:done${EOL}`) } } process.stderr.write("Database migration complete." + EOL) } }) .usage("\n" + UI.logo()) .completion("completion", "generate shell completion script") .command(AcpCommand) .command(McpCommand) .command(TuiThreadCommand) .command(AttachCommand) .command(RunCommand) .command(GenerateCommand) .command(DebugCommand) .command(AuthCommand) .command(AgentCommand) .command(UpgradeCommand) .command(UninstallCommand) .command(ServeCommand) .command(WebCommand) .command(ModelsCommand) .command(StatsCommand) .command(ExportCommand) .command(ImportCommand) // .command(GithubCommand) // kilocode_change (Disabled until backend is ready) .command(PrCommand) .command(SessionCommand) .command(DbCommand) .fail((msg, err) => { if ( msg?.startsWith("Unknown argument") || msg?.startsWith("Not enough non-option arguments") || msg?.startsWith("Invalid values:") ) { if (err) throw err cli.showHelp("log") } if (err) throw err process.exit(1) }) .strict() try { await cli.parse() } catch (e) { let data: Record = {} if (e instanceof NamedError) { const obj = e.toObject() Object.assign(data, { ...obj.data, }) } if (e instanceof Error) { Object.assign(data, { name: e.name, message: e.message, cause: e.cause?.toString(), stack: e.stack, }) } if (e instanceof ResolveMessage) { Object.assign(data, { name: e.name, message: e.message, code: e.code, specifier: e.specifier, referrer: e.referrer, position: e.position, importKind: e.importKind, }) } Log.Default.error("fatal", data) const formatted = FormatError(e) if (formatted) UI.error(formatted) if (formatted === undefined) { UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL) process.stderr.write((e instanceof Error ? e.message : String(e)) + EOL) } process.exitCode = 1 } finally { // kilocode_change start - Track CLI exit and shutdown telemetry const exitCode = typeof process.exitCode === "number" ? process.exitCode : undefined Telemetry.trackCliExit(exitCode) await Telemetry.shutdown() // kilocode_change end await Instance.disposeAll() // kilocode_change - safety net disposal (no-op if already disposed) // Some subprocesses don't react properly to SIGTERM and similar signals. // Most notably, some docker-container-based MCP servers don't handle such signals unless // run using `docker run --init`. // Explicitly exit to avoid any hanging subprocesses. process.exit() }