Merged main into feat/use-sdk

This commit is contained in:
Imanol Maiztegui
2026-03-02 19:35:41 +01:00
293 changed files with 2390 additions and 619 deletions
+12 -6
View File
@@ -25,11 +25,20 @@ if (envPath) {
const scriptPath = fs.realpathSync(__filename)
const scriptDir = path.dirname(scriptPath)
//
// kilocode_change start - fall through to findBinary() if cached binary fails
const cached = path.join(scriptDir, ".kilo")
if (fs.existsSync(cached)) {
run(cached)
const result = childProcess.spawnSync(cached, process.argv.slice(2), {
stdio: "inherit",
})
if (!result.error) {
const code = typeof result.status === "number" ? result.status : 0
process.exit(code)
}
// cached binary failed (e.g. wrong platform/arch, missing dynamic linker),
// fall through to findBinary() which has better variant detection
}
// kilocode_change end
const platformMap = {
darwin: "darwin",
@@ -166,9 +175,6 @@ function findBinary(startDir) {
}
}
const scriptPath = fs.realpathSync(__filename)
const scriptDir = path.dirname(scriptPath)
const resolved = findBinary(scriptDir)
if (!resolved) {
console.error(
@@ -179,4 +185,4 @@ if (!resolved) {
process.exit(1)
}
run(resolved)
run(resolved)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.0.32",
"version": "7.0.33",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
+21
View File
@@ -188,6 +188,27 @@ for (const item of targets) {
},
})
// kilocode_change start - fix Nix-specific ELF interpreter paths for Linux binaries
if (item.os === "linux") {
const interpreters: Record<string, string> = {
x64: "/lib64/ld-linux-x86-64.so.2",
arm64: "/lib/ld-linux-aarch64.so.1",
"x64-musl": "/lib/ld-musl-x86_64.so.1",
"arm64-musl": "/lib/ld-musl-aarch64.so.1",
}
const key = item.abi === "musl" ? `${item.arch}-musl` : item.arch
const interpreter = interpreters[key]
if (interpreter) {
try {
await $`patchelf --set-interpreter ${interpreter} dist/${name}/bin/kilo`
console.log(`patched interpreter for ${name} -> ${interpreter}`)
} catch {
console.warn(`patchelf not available, skipping interpreter fix for ${name}`)
}
}
}
// kilocode_change end
await $`rm -rf ./dist/${name}/bin/tui`
await Bun.file(`dist/${name}/package.json`).write(
JSON.stringify(
+125 -104
View File
@@ -3,129 +3,150 @@
import fs from "fs"
import path from "path"
import os from "os"
import childProcess from "child_process"
import { fileURLToPath } from "url"
import { createRequire } from "module"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url)
// kilocode_change start - variant detection matching bin/kilo logic
const platformMap = {
darwin: "darwin",
linux: "linux",
win32: "windows",
}
const archMap = {
x64: "x64",
arm64: "arm64",
arm: "arm",
}
function detectPlatformAndArch() {
// Map platform names
let platform
switch (os.platform()) {
case "darwin":
platform = "darwin"
break
case "linux":
platform = "linux"
break
case "win32":
platform = "windows"
break
default:
platform = os.platform()
break
}
// Map architecture names
let arch
switch (os.arch()) {
case "x64":
arch = "x64"
break
case "arm64":
arch = "arm64"
break
case "arm":
arch = "arm"
break
default:
arch = os.arch()
break
}
const platform = platformMap[os.platform()] || os.platform()
const arch = archMap[os.arch()] || os.arch()
return { platform, arch }
}
function findBinary() {
function supportsAvx2() {
const { platform, arch } = detectPlatformAndArch()
const packageName = `@kilocode/cli-${platform}-${arch}`
const binaryName = platform === "windows" ? "kilo.exe" : "kilo"
if (arch !== "x64") return false
try {
// Use require.resolve to find the package
const packageJsonPath = require.resolve(`${packageName}/package.json`)
const packageDir = path.dirname(packageJsonPath)
const binaryPath = path.join(packageDir, "bin", binaryName)
if (!fs.existsSync(binaryPath)) {
throw new Error(`Binary not found at ${binaryPath}`)
}
return { binaryPath, binaryName }
} catch (error) {
throw new Error(`Could not find package ${packageName}: ${error.message}`)
}
}
function prepareBinDirectory(binaryName) {
const binDir = path.join(__dirname, "bin")
const targetPath = path.join(binDir, binaryName)
// Ensure bin directory exists
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true })
}
// Remove existing binary/symlink if it exists
if (fs.existsSync(targetPath)) {
fs.unlinkSync(targetPath)
}
return { binDir, targetPath }
}
function symlinkBinary(sourcePath, binaryName) {
const { targetPath } = prepareBinDirectory(binaryName)
fs.symlinkSync(sourcePath, targetPath)
console.log(`kilo binary symlinked: ${targetPath} -> ${sourcePath}`)
// Verify the file exists after operation
if (!fs.existsSync(targetPath)) {
throw new Error(`Failed to symlink binary to ${targetPath}`)
}
}
async function main() {
try {
if (os.platform() === "win32") {
// On Windows, the .exe is already included in the package and bin field points to it
// No postinstall setup needed
console.log("Windows detected: binary setup not needed (using packaged .exe)")
return
}
// On non-Windows platforms, just verify the binary package exists
// Don't replace the wrapper script - it handles binary execution
const { binaryPath } = findBinary()
const target = path.join(__dirname, "bin", ".kilo") // kilocode_change
if (fs.existsSync(target)) fs.unlinkSync(target)
if (platform === "linux") {
try {
fs.linkSync(binaryPath, target)
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
} catch {
fs.copyFileSync(binaryPath, target)
return false
}
fs.chmodSync(target, 0o755)
} catch (error) {
console.error("Failed to setup kilo binary:", error.message)
process.exit(1)
}
if (platform === "darwin") {
try {
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
encoding: "utf8",
timeout: 1500,
})
if (result.status !== 0) return false
return (result.stdout || "").trim() === "1"
} catch {
return false
}
}
return false
}
function isMusl() {
try {
if (fs.existsSync("/etc/alpine-release")) return true
} catch {
// ignore
}
try {
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase()
if (text.includes("musl")) return true
} catch {
// ignore
}
return false
}
function getPackageNames() {
const { platform, arch } = detectPlatformAndArch()
const base = `@kilocode/cli-${platform}-${arch}`
const avx2 = supportsAvx2()
const baseline = arch === "x64" && !avx2
if (platform === "linux") {
const musl = isMusl()
if (musl) {
if (arch === "x64") {
if (baseline) return [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
return [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
}
return [`${base}-musl`, base]
}
if (arch === "x64") {
if (baseline) return [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
return [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
}
return [base, `${base}-musl`]
}
if (arch === "x64") {
if (baseline) return [`${base}-baseline`, base]
return [base, `${base}-baseline`]
}
return [base]
}
function findBinary() {
const { platform } = detectPlatformAndArch()
const binaryName = platform === "windows" ? "kilo.exe" : "kilo"
const names = getPackageNames()
for (const packageName of names) {
try {
const packageJsonPath = require.resolve(`${packageName}/package.json`)
const packageDir = path.dirname(packageJsonPath)
const binaryPath = path.join(packageDir, "bin", binaryName)
if (fs.existsSync(binaryPath)) {
return { binaryPath, binaryName }
}
} catch {
// package not installed, try next variant
}
}
throw new Error(`Could not find any binary package. Tried: ${names.map((n) => `"${n}"`).join(", ")}`)
}
// kilocode_change end
function main() {
if (os.platform() === "win32") {
// On Windows, the .exe is already included in the package and bin field points to it
console.log("Windows detected: binary setup not needed (using packaged .exe)")
return
}
const { binaryPath } = findBinary()
const target = path.join(__dirname, "bin", ".kilo") // kilocode_change
if (fs.existsSync(target)) fs.unlinkSync(target)
try {
fs.linkSync(binaryPath, target)
} catch {
fs.copyFileSync(binaryPath, target)
}
fs.chmodSync(target, 0o755)
}
try {
main()
} catch (error) {
console.error("Postinstall script error:", error.message)
process.exit(0)
console.error("Failed to setup kilo binary:", error.message)
process.exit(1)
}
+5 -1
View File
@@ -1,6 +1,7 @@
import type { Argv } from "yargs"
import type { Session as SDKSession, Message, Part } from "@kilocode/sdk/v2"
import { Session } from "../../session"
import { Bus } from "../../bus"
import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap"
import { Database } from "../../storage/db"
@@ -131,7 +132,10 @@ export const ImportCommand = cmd({
return
}
Database.use((db) => db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run())
Database.use((db) => {
db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run()
Database.effect(() => Bus.publish(Session.Event.Created, { info: exportData.info }))
})
for (const msg of exportData.messages) {
Database.use((db) =>
@@ -60,6 +60,7 @@ export namespace IngestQueue {
getClient: () => Promise<Client | undefined>
onAuthError?: () => void
log: {
info?: (message: string, data: Record<string, unknown>) => void
error: (message: string, data: Record<string, unknown>) => void
}
now?: () => number
@@ -198,6 +199,16 @@ export namespace IngestQueue {
const client = await options.getClient()
if (!client) return
if (options.log.info) {
const types = items.map((d) => d.type).join(",")
options.log.info("ingest flush", {
sessionId,
url: `${client.url}${share.ingestPath}?v=1`,
items: items.length,
types,
})
}
const response = await client
.fetch(`${client.url}${share.ingestPath}?v=1`, {
method: "POST",
@@ -218,12 +229,13 @@ export namespace IngestQueue {
const delay = backoff(count)
retry.set(sessionId, { count, until: now() + delay })
options.log.error("share sync failed", { sessionId, error: "network", retryInMs: delay })
options.log.error("share sync failed", { sessionId, error: "network", attempt: count, retryInMs: delay })
enqueue(sessionId, items, "fill", now() + delay)
return
}
if (response.ok) {
options.log.info?.("ingest flush ok", { sessionId, items: items.length })
retry.delete(sessionId)
return
}
@@ -265,6 +277,7 @@ export namespace IngestQueue {
sessionId,
status: response.status,
statusText: response.statusText,
attempt: count,
retryInMs: delay,
})
enqueue(sessionId, items, "fill", now() + delay)
@@ -282,6 +295,11 @@ export namespace IngestQueue {
const client = await options.getClient()
if (!client) return
if (options.log.info) {
const types = data.map((d) => d.type).join(",")
options.log.info("ingest sync", { sessionId, types })
}
const until = retry.get(sessionId)?.until ?? 0
const base = queue.get(sessionId)?.due ?? now() + 1000
const due = Math.max(base, until)
@@ -107,10 +107,19 @@ export namespace KiloSessions {
})
}
const shareDisabled = process.env["KILO_DISABLE_SHARE"] === "true" || process.env["KILO_DISABLE_SHARE"] === "1"
const ingestDisabled =
process.env["KILO_DISABLE_SESSION_INGEST"] === "true" || process.env["KILO_DISABLE_SESSION_INGEST"] === "1"
const debugIngest =
process.env["KILO_DEBUG_SESSION_INGEST"] === "true" || process.env["KILO_DEBUG_SESSION_INGEST"] === "1"
const ingest = IngestQueue.create({
getShare: async (sessionId) => get(sessionId).catch(() => undefined),
getClient,
log,
log: {
...(debugIngest ? { info: log.info.bind(log) } : {}),
error: log.error.bind(log),
},
onAuthError: () => {
// Non-retryable until credentials are fixed.
// Clearing caches prevents repeated use of a now-invalid token/client.
@@ -118,10 +127,6 @@ export namespace KiloSessions {
},
})
const shareDisabled = process.env["KILO_DISABLE_SHARE"] === "true" || process.env["KILO_DISABLE_SHARE"] === "1"
const ingestDisabled =
process.env["KILO_DISABLE_SESSION_INGEST"] === "true" || process.env["KILO_DISABLE_SESSION_INGEST"] === "1"
export async function init() {
if (ingestDisabled) return
@@ -42,7 +42,7 @@ export function KiloNews() {
if (!isKiloConnected()) return
const result = await sdk.client.kilo.notifications()
const items = result.data
const items = result.data?.filter(({ showIn }) => !showIn || showIn.includes("cli"))
if (items && items.length > 0) {
setNotifications(items)
}
@@ -111,7 +111,11 @@ export namespace PlanFollowup {
export const ANSWER_NEW_SESSION = "Start new session"
export const ANSWER_CONTINUE = "Continue here"
async function resolvePlan(input: { assistant?: MessageV2.WithParts; messages: MessageV2.WithParts[]; sessionID: string }) {
async function resolvePlan(input: {
assistant?: MessageV2.WithParts
messages: MessageV2.WithParts[]
sessionID: string
}) {
// Fast path: check the last assistant message's text first (avoids array scanning)
if (input.assistant) {
const text = toText(input.assistant)
@@ -121,9 +125,7 @@ export namespace PlanFollowup {
// Fallback: scan all assistant messages after the last user message (handles
// cases where plan text is on an earlier assistant and the last one is empty)
const lastUserIdx = input.messages.findLastIndex((m) => m.info.role === "user")
const assistantMessages = input.messages
.slice(lastUserIdx + 1)
.filter((m) => m.info.role === "assistant")
const assistantMessages = input.messages.slice(lastUserIdx + 1).filter((m) => m.info.role === "assistant")
const text = assistantMessages.map(toText).filter(Boolean).join("\n\n").trim()
if (text) return text