mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge branch 'main' into feat/vscode-model-search-fuzzy
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
- ⚡ Inline autocomplete suggestions
|
||||
- 🤖 Latest AI models
|
||||
- 🎁 API keys optional
|
||||
- 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.2
|
||||
- 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.4
|
||||
|
||||
## Quick Links
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
## Get Started in Visual Studio Code
|
||||
|
||||
1. Install the Kilo Code extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code).
|
||||
2. Create your account to access 500+ cutting-edge AI models including Gemini 3 Pro, Claude 4.5 Sonnet & Opus, and GPT-5 – with transparent pricing that matches provider rates exactly.
|
||||
2. Create your account to access 500+ cutting-edge AI models including Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.4 – with transparent pricing that matches provider rates exactly.
|
||||
3. Start coding with AI that adapts to your workflow. Watch our quick-start guide to see Kilo in action:
|
||||
|
||||
<a href="https://youtu.be/pqGfYXgrhig"><img src="https://img.youtube.com/vi/pqGfYXgrhig/maxresdefault.jpg" alt="Watch the video" width="640" height="360"></a>
|
||||
|
||||
@@ -126,6 +126,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
private cachedMcpStatusMessage: unknown = null
|
||||
/** Ref-count of in-flight handleUpdateConfig calls; prevents fetchAndSendConfig from sending stale data */
|
||||
private pending = 0
|
||||
private configWarningsShown = false
|
||||
/** Cached notificationsLoaded payload */
|
||||
private cachedNotificationsMessage: unknown = null
|
||||
private pendingReviewComments: { comments: unknown[]; autoSend: boolean }[] = []
|
||||
@@ -1055,6 +1056,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.postMessage({ type: "connectionState", state })
|
||||
|
||||
if (state === "connected") {
|
||||
// Fire config warnings independently so a failure in the
|
||||
// sequential await chain doesn't prevent warnings from being shown
|
||||
void this.checkConfigWarnings("state")
|
||||
try {
|
||||
// Profile fetch is best-effort — returns 401 when user isn't logged into gateway.
|
||||
const sdkClient = this.client
|
||||
@@ -1130,6 +1134,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
|
||||
this.postMessage({ type: "connectionState", state: this.connectionState })
|
||||
|
||||
// connect() can resolve after SSE reaches "connected" but before this
|
||||
// provider subscribes to onStateChange(). In that case the initial
|
||||
// connected callback is missed, so run the warning check here too.
|
||||
if (this.connectionState === "connected") {
|
||||
void this.checkConfigWarnings("init")
|
||||
}
|
||||
|
||||
await this.syncWebviewState("initializeConnection")
|
||||
await this.flushPendingSessionRefresh("initializeConnection")
|
||||
|
||||
@@ -1965,6 +1977,49 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch config warnings from the server and display a single consolidated
|
||||
* VS Code warning with a "Show Details" action button.
|
||||
* Only shown once per provider lifecycle (flag resets on dispose/re-create, not on SSE reconnect).
|
||||
*/
|
||||
private async checkConfigWarnings(from: string): Promise<void> {
|
||||
if (this.configWarningsShown) {
|
||||
console.log("[Kilo New] KiloProvider: config warnings already shown", { from })
|
||||
return
|
||||
}
|
||||
if (!this.client) {
|
||||
console.log("[Kilo New] KiloProvider: config warnings skipped (no client)", { from })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const dir = this.getWorkspaceDirectory()
|
||||
console.log("[Kilo New] KiloProvider: checking config warnings", { from, dir })
|
||||
const result = await this.client.config.warnings({ directory: dir })
|
||||
const list = result?.data ?? []
|
||||
console.log("[Kilo New] KiloProvider: config warnings fetched", { from, count: list.length })
|
||||
if (list.length === 0) return
|
||||
this.configWarningsShown = true
|
||||
|
||||
const first = list[0]!
|
||||
const summary = list.length === 1 ? first.message : `${first.message} (and ${list.length - 1} more)`
|
||||
console.warn("[Kilo New] KiloProvider: showing config warnings", { from, count: list.length, path: first.path })
|
||||
|
||||
const action = await vscode.window.showWarningMessage(`Config: ${summary}`, "Show Details")
|
||||
if (action === "Show Details") {
|
||||
const lines = list.map((w) => {
|
||||
const base = `${w.path}\n ${w.message}`
|
||||
return w.detail ? `${base}\n ${w.detail}` : base
|
||||
})
|
||||
const channel = vscode.window.createOutputChannel("Kilo Config Warnings")
|
||||
channel.clear()
|
||||
channel.appendLine(lines.join("\n\n"))
|
||||
channel.show()
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[Kilo New] KiloProvider: checkConfigWarnings failed:", { from, err })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Kilo news/notifications and send to webview.
|
||||
* Uses the cached message pattern so the webview gets data immediately on refresh.
|
||||
|
||||
@@ -307,6 +307,11 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
sendAllToChat()
|
||||
}
|
||||
|
||||
const handleExpandAll = () => {
|
||||
const allOpen = open().length === props.diffs.length
|
||||
setOpen(allOpen ? [] : props.diffs.map((d) => d.file))
|
||||
}
|
||||
|
||||
const totals = createMemo(() => ({
|
||||
files: props.diffs.length,
|
||||
additions: props.diffs.reduce((sum, diff) => sum + diff.additions, 0),
|
||||
@@ -354,6 +359,28 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
<div class="am-diff-header-actions">
|
||||
<Show when={props.diffs.length > 0}>
|
||||
<Tooltip
|
||||
value={
|
||||
open().length === props.diffs.length
|
||||
? t("ui.sessionReview.collapseAll")
|
||||
: t("ui.sessionReview.expandAll")
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<IconButton
|
||||
icon="chevron-grabber-vertical"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={
|
||||
open().length === props.diffs.length
|
||||
? t("ui.sessionReview.collapseAll")
|
||||
: t("ui.sessionReview.expandAll")
|
||||
}
|
||||
onClick={handleExpandAll}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Show when={props.onExpand}>
|
||||
<Tooltip value={t("command.review.toggle")} placement="bottom">
|
||||
<IconButton
|
||||
|
||||
@@ -244,7 +244,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
window.addEventListener("compactSession", onCompact)
|
||||
onCleanup(() => window.removeEventListener("compactSession", onCompact))
|
||||
|
||||
const isBusy = () => session.status() === "busy"
|
||||
const isBusy = () => session.status() !== "idle"
|
||||
const isDisabled = () => !server.isConnected()
|
||||
const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0
|
||||
const canSend = () => hasInput() && !isDisabled() && !props.blocked?.()
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// kilocode_change - new file
|
||||
import { EOL } from "os"
|
||||
import { Config } from "../../config/config"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { cmd } from "./cmd"
|
||||
import { UI } from "../ui"
|
||||
|
||||
export const ConfigCommand = cmd({
|
||||
command: "config",
|
||||
describe: "configuration tools",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command({
|
||||
command: "check",
|
||||
describe: "check configuration for warnings and errors",
|
||||
async handler() {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const list = await Config.warnings()
|
||||
if (list.length === 0) {
|
||||
process.stdout.write("No config warnings." + EOL)
|
||||
return
|
||||
}
|
||||
const S = UI.Style
|
||||
for (const warning of list) {
|
||||
process.stderr.write(S.TEXT_WARNING_BOLD + warning.path + S.TEXT_NORMAL + EOL)
|
||||
process.stderr.write(" " + warning.message + EOL)
|
||||
if (warning.detail) {
|
||||
for (const line of warning.detail.split("\n")) {
|
||||
process.stderr.write(" " + S.TEXT_DIM + line + S.TEXT_NORMAL + EOL)
|
||||
}
|
||||
}
|
||||
process.stderr.write(EOL)
|
||||
}
|
||||
process.exitCode = 1
|
||||
})
|
||||
},
|
||||
})
|
||||
.demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
@@ -636,6 +636,7 @@ export function Prompt(props: PromptProps) {
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
toast.dismiss() // kilocode_change - dismiss persistent config warning on first submit
|
||||
history.append({
|
||||
...store.prompt,
|
||||
mode: currentMode,
|
||||
|
||||
@@ -29,6 +29,7 @@ import { batch, onMount } from "solid-js"
|
||||
import { Log } from "@/util/log"
|
||||
import type { Path } from "@kilocode/sdk"
|
||||
import type { Workspace } from "@kilocode/sdk/v2"
|
||||
import { useToast } from "../ui/toast" // kilocode_change
|
||||
|
||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
@@ -106,6 +107,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const sdk = useSDK()
|
||||
const toast = useToast() // kilocode_change
|
||||
|
||||
const fullSyncedSessions = new Set<string>() // kilocode_change
|
||||
|
||||
@@ -464,6 +466,23 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
sdk.client.vcs.get().then((x) => setStore("vcs", reconcile(x.data))),
|
||||
sdk.client.path.get().then((x) => setStore("path", reconcile(x.data!))),
|
||||
syncWorkspaces(),
|
||||
// kilocode_change start - show config warnings as persistent toast
|
||||
sdk.client.config
|
||||
.warnings()
|
||||
.then((x) => {
|
||||
const list = x.data ?? []
|
||||
if (list.length === 0) return
|
||||
const first = list[0]
|
||||
const suffix = list.length > 1 ? ` (and ${list.length - 1} more)` : ""
|
||||
toast.show({
|
||||
title: "Config Warning",
|
||||
message: first.message + suffix,
|
||||
variant: "warning",
|
||||
duration: 0,
|
||||
})
|
||||
})
|
||||
.catch(() => {}),
|
||||
// kilocode_change end
|
||||
]).then(() => {
|
||||
setStore("status", "complete")
|
||||
})
|
||||
|
||||
@@ -60,10 +60,21 @@ function init() {
|
||||
const { duration, ...currentToast } = parsedOptions
|
||||
setStore("currentToast", currentToast)
|
||||
if (timeoutHandle) clearTimeout(timeoutHandle)
|
||||
timeoutHandle = setTimeout(() => {
|
||||
setStore("currentToast", null)
|
||||
}, duration).unref()
|
||||
// kilocode_change start
|
||||
timeoutHandle = null
|
||||
if (duration && duration > 0) {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
setStore("currentToast", null)
|
||||
timeoutHandle = null
|
||||
}, duration).unref()
|
||||
}
|
||||
},
|
||||
dismiss() {
|
||||
if (timeoutHandle) clearTimeout(timeoutHandle)
|
||||
timeoutHandle = null
|
||||
setStore("currentToast", null)
|
||||
},
|
||||
// kilocode_change end
|
||||
error: (err: any) => {
|
||||
if (err instanceof Error)
|
||||
return toast.show({
|
||||
|
||||
@@ -52,6 +52,15 @@ export namespace Config {
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
// kilocode_change start
|
||||
export const Warning = z.object({
|
||||
path: z.string(),
|
||||
message: z.string(),
|
||||
detail: z.string().optional(),
|
||||
})
|
||||
export type Warning = z.infer<typeof Warning>
|
||||
// kilocode_change end
|
||||
|
||||
// Managed settings directory for enterprise deployments (highest priority, admin-controlled)
|
||||
// These settings override all user and project settings
|
||||
function systemManagedConfigDir(): string {
|
||||
@@ -85,6 +94,18 @@ export namespace Config {
|
||||
|
||||
// kilocode_change start — capture init so resetState() can invalidate the cache entry
|
||||
const stateInit = async () => {
|
||||
// kilocode_change end
|
||||
// kilocode_change start
|
||||
const warnings: Warning[] = []
|
||||
const caught = (err: unknown, source: string) => {
|
||||
const w = toWarning(err)
|
||||
if (w) {
|
||||
warnings.push(w)
|
||||
log.warn("skipped config due to error", { source, err })
|
||||
return
|
||||
}
|
||||
throw err
|
||||
}
|
||||
// kilocode_change end
|
||||
const auth = await Auth.all()
|
||||
|
||||
@@ -200,24 +221,34 @@ export namespace Config {
|
||||
for (const [key, value] of Object.entries(auth)) {
|
||||
if (value.type === "wellknown") {
|
||||
const url = key.replace(/\/+$/, "")
|
||||
process.env[value.key] = value.token
|
||||
log.debug("fetching remote config", { url: `${url}/.well-known/opencode` })
|
||||
const response = await fetch(`${url}/.well-known/opencode`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
|
||||
// kilocode_change start
|
||||
const source = `${url}/.well-known/opencode`
|
||||
try {
|
||||
process.env[value.key] = value.token
|
||||
log.debug("fetching remote config", { url: source })
|
||||
const response = await fetch(source)
|
||||
if (!response.ok) {
|
||||
throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
|
||||
}
|
||||
const wellknown = (await response.json()) as any
|
||||
const remoteConfig = wellknown.config ?? {}
|
||||
// Add $schema to prevent load() from trying to write back to a non-existent file
|
||||
if (!remoteConfig.$schema) remoteConfig.$schema = "https://app.kilo.ai/config.json"
|
||||
result = mergeConfigConcatArrays(
|
||||
result,
|
||||
await load(JSON.stringify(remoteConfig), {
|
||||
dir: path.dirname(source),
|
||||
source,
|
||||
}),
|
||||
)
|
||||
log.debug("loaded remote config from well-known", { url })
|
||||
} catch (err) {
|
||||
const w = toWarning(err)
|
||||
if (w) warnings.push(w)
|
||||
else warnings.push({ path: source, message: err instanceof Error ? err.message : String(err) })
|
||||
log.warn("skipped remote config due to error", { url, err })
|
||||
// kilocode_change end
|
||||
}
|
||||
const wellknown = (await response.json()) as any
|
||||
const remoteConfig = wellknown.config ?? {}
|
||||
// Add $schema to prevent load() from trying to write back to a non-existent file
|
||||
if (!remoteConfig.$schema) remoteConfig.$schema = "https://app.kilo.ai/config.json" // kilocode_change
|
||||
result = mergeConfigConcatArrays(
|
||||
result,
|
||||
await load(JSON.stringify(remoteConfig), {
|
||||
dir: path.dirname(`${url}/.well-known/opencode`),
|
||||
source: `${url}/.well-known/opencode`,
|
||||
}),
|
||||
)
|
||||
log.debug("loaded remote config from well-known", { url })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,21 +257,37 @@ export namespace Config {
|
||||
}
|
||||
|
||||
// Global user config overrides remote config.
|
||||
result = mergeConfigConcatArrays(result, await global())
|
||||
// kilocode_change start
|
||||
try {
|
||||
result = mergeConfigConcatArrays(result, await global())
|
||||
} catch (err) {
|
||||
caught(err, "global config")
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// Custom config path overrides global config.
|
||||
if (Flag.KILO_CONFIG) {
|
||||
result = mergeConfigConcatArrays(result, await loadFile(Flag.KILO_CONFIG))
|
||||
log.debug("loaded custom config", { path: Flag.KILO_CONFIG })
|
||||
// kilocode_change start
|
||||
try {
|
||||
result = mergeConfigConcatArrays(result, await loadFile(Flag.KILO_CONFIG))
|
||||
log.debug("loaded custom config", { path: Flag.KILO_CONFIG })
|
||||
} catch (err) {
|
||||
caught(err, Flag.KILO_CONFIG)
|
||||
}
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
// Project config overrides global and remote config.
|
||||
if (!Flag.KILO_DISABLE_PROJECT_CONFIG) {
|
||||
// kilocode_change start
|
||||
for (const file of ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json"]) {
|
||||
// kilocode_change end
|
||||
result = mergeConfigConcatArrays(result, await loadFile(file))
|
||||
try {
|
||||
result = mergeConfigConcatArrays(result, await loadFile(file))
|
||||
} catch (err) {
|
||||
caught(err, file)
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
result.agent = result.agent || {}
|
||||
@@ -265,14 +312,18 @@ export namespace Config {
|
||||
dir === Flag.KILO_CONFIG_DIR
|
||||
) {
|
||||
for (const file of ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json"]) {
|
||||
// kilocode_change end
|
||||
log.debug(`loading config from ${path.join(dir, file)}`)
|
||||
result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
|
||||
try {
|
||||
result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
|
||||
} catch (err) {
|
||||
caught(err, path.join(dir, file))
|
||||
}
|
||||
// to satisfy the type checker
|
||||
result.agent ??= {}
|
||||
result.mode ??= {}
|
||||
result.plugin ??= []
|
||||
}
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
deps.push(
|
||||
@@ -282,22 +333,34 @@ export namespace Config {
|
||||
}),
|
||||
)
|
||||
|
||||
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
|
||||
result.agent = mergeDeep(result.agent, await loadAgent(dir))
|
||||
result.agent = mergeDeep(result.agent, await loadMode(dir))
|
||||
result.plugin.push(...(await loadPlugin(dir)))
|
||||
// kilocode_change start
|
||||
try {
|
||||
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir, warnings))
|
||||
result.agent = mergeDeep(result.agent, await loadAgent(dir, warnings))
|
||||
result.agent = mergeDeep(result.agent, await loadMode(dir, warnings))
|
||||
result.plugin.push(...(await loadPlugin(dir)))
|
||||
} catch (err: unknown) {
|
||||
log.error("failed to load config directory", { dir, err })
|
||||
}
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
// Inline config content overrides all non-managed config sources.
|
||||
if (process.env.KILO_CONFIG_CONTENT) {
|
||||
result = mergeConfigConcatArrays(
|
||||
result,
|
||||
await load(process.env.KILO_CONFIG_CONTENT, {
|
||||
dir: Instance.directory,
|
||||
source: "KILO_CONFIG_CONTENT",
|
||||
}),
|
||||
)
|
||||
log.debug("loaded custom config from KILO_CONFIG_CONTENT")
|
||||
// kilocode_change start
|
||||
try {
|
||||
result = mergeConfigConcatArrays(
|
||||
result,
|
||||
await load(process.env.KILO_CONFIG_CONTENT, {
|
||||
dir: Instance.directory,
|
||||
source: "KILO_CONFIG_CONTENT",
|
||||
}),
|
||||
)
|
||||
log.debug("loaded custom config from KILO_CONFIG_CONTENT")
|
||||
} catch (err) {
|
||||
caught(err, "KILO_CONFIG_CONTENT")
|
||||
}
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
// Load managed config files last (highest priority) - enterprise admin-controlled
|
||||
@@ -307,9 +370,9 @@ export namespace Config {
|
||||
if (existsSync(managedDir)) {
|
||||
// kilocode_change start
|
||||
for (const file of ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json"]) {
|
||||
// kilocode_change end
|
||||
result = mergeConfigConcatArrays(result, await loadFile(path.join(managedDir, file)))
|
||||
}
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
// Migrate deprecated mode field to agent field
|
||||
@@ -361,6 +424,7 @@ export namespace Config {
|
||||
config: result,
|
||||
directories,
|
||||
deps,
|
||||
warnings, // kilocode_change
|
||||
}
|
||||
}
|
||||
// kilocode_change start — create state from named init so resetState() can invalidate it
|
||||
@@ -463,7 +527,64 @@ export namespace Config {
|
||||
return ext.length ? file.slice(0, -ext.length) : file
|
||||
}
|
||||
|
||||
async function loadCommand(dir: string) {
|
||||
// kilocode_change start
|
||||
function toWarning(err: unknown): Warning | undefined {
|
||||
if (ConfigPaths.JsonError.isInstance(err))
|
||||
return {
|
||||
path: err.data.path,
|
||||
message: `Config file at ${err.data.path} is not valid JSON(C)`,
|
||||
detail: err.data.message || undefined,
|
||||
}
|
||||
if (ConfigPaths.InvalidError.isInstance(err)) {
|
||||
const text = err.data.issues ? detail(err.data.issues) : err.data.message
|
||||
return {
|
||||
path: err.data.path,
|
||||
message: text
|
||||
? `Configuration is invalid at ${err.data.path}: ${text}`
|
||||
: `Configuration is invalid at ${err.data.path}`,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function detail(issues: z.core.$ZodIssue[]) {
|
||||
return issues
|
||||
.map((issue) => {
|
||||
const loc = issue.path.map(String).join(".")
|
||||
if (!loc) return issue.message
|
||||
return `${loc}: ${issue.message}`
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
async function invalid(
|
||||
kind: "agent" | "command",
|
||||
item: string,
|
||||
issues: z.core.$ZodIssue[],
|
||||
cause: Error,
|
||||
warnings?: Warning[],
|
||||
) {
|
||||
const text = detail(issues)
|
||||
const message = text ? `Config file at ${item} is invalid: ${text}` : `Config file at ${item} is invalid`
|
||||
const err = new InvalidError({ path: item, issues }, { cause })
|
||||
if (warnings) warnings.push({ path: item, message, detail: text || undefined })
|
||||
try {
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
} catch (e) {
|
||||
log.warn("could not publish session error", { message, err: e })
|
||||
}
|
||||
if (kind === "command") {
|
||||
log.error("failed to load command", { command: item, err, message })
|
||||
return
|
||||
}
|
||||
log.error("failed to load agent", { agent: item, err, message })
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start
|
||||
async function loadCommand(dir: string, warnings?: Warning[]) {
|
||||
// kilocode_change end
|
||||
const result: Record<string, Command> = {}
|
||||
for (const item of await Glob.scan("{command,commands}/**/*.md", {
|
||||
cwd: dir,
|
||||
@@ -475,10 +596,17 @@ export namespace Config {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse command ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
// kilocode_change start
|
||||
if (warnings) warnings.push({ path: item, message })
|
||||
try {
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
} catch (e) {
|
||||
log.warn("could not publish session error", { message, err: e })
|
||||
}
|
||||
log.error("failed to load command", { command: item, err })
|
||||
return undefined
|
||||
// kilocode_change end
|
||||
})
|
||||
if (!md) continue
|
||||
|
||||
@@ -505,12 +633,16 @@ export namespace Config {
|
||||
result[config.name] = parsed.data
|
||||
continue
|
||||
}
|
||||
throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
|
||||
// kilocode_change start
|
||||
await invalid("command", item, parsed.error.issues, parsed.error, warnings)
|
||||
// kilocode_change end
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function loadAgent(dir: string) {
|
||||
// kilocode_change start
|
||||
async function loadAgent(dir: string, warnings?: Warning[]) {
|
||||
// kilocode_change end
|
||||
const result: Record<string, Agent> = {}
|
||||
|
||||
for (const item of await Glob.scan("{agent,agents}/**/*.md", {
|
||||
@@ -523,10 +655,17 @@ export namespace Config {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse agent ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
// kilocode_change start
|
||||
if (warnings) warnings.push({ path: item, message })
|
||||
try {
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
} catch (e) {
|
||||
log.warn("could not publish session error", { message, err: e })
|
||||
}
|
||||
log.error("failed to load agent", { agent: item, err })
|
||||
return undefined
|
||||
// kilocode_change end
|
||||
})
|
||||
if (!md) continue
|
||||
|
||||
@@ -555,12 +694,16 @@ export namespace Config {
|
||||
result[config.name] = parsed.data
|
||||
continue
|
||||
}
|
||||
throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
|
||||
// kilocode_change start
|
||||
await invalid("agent", item, parsed.error.issues, parsed.error, warnings)
|
||||
// kilocode_change end
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function loadMode(dir: string) {
|
||||
// kilocode_change start
|
||||
async function loadMode(dir: string, warnings?: Warning[]) {
|
||||
// kilocode_change end
|
||||
const result: Record<string, Agent> = {}
|
||||
for (const item of await Glob.scan("{mode,modes}/*.md", {
|
||||
cwd: dir,
|
||||
@@ -572,10 +715,17 @@ export namespace Config {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse mode ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
// kilocode_change start
|
||||
if (warnings) warnings.push({ path: item, message })
|
||||
try {
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
} catch (e) {
|
||||
log.warn("could not publish session error", { message, err: e })
|
||||
}
|
||||
log.error("failed to load mode", { mode: item, err })
|
||||
return undefined
|
||||
// kilocode_change end
|
||||
})
|
||||
if (!md) continue
|
||||
|
||||
@@ -592,6 +742,9 @@ export namespace Config {
|
||||
}
|
||||
continue
|
||||
}
|
||||
// kilocode_change start
|
||||
await invalid("agent", item, parsed.error.issues, parsed.error, warnings)
|
||||
// kilocode_change end
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1497,6 +1650,12 @@ export namespace Config {
|
||||
return state().then((x) => x.config)
|
||||
}
|
||||
|
||||
// kilocode_change start
|
||||
export async function warnings() {
|
||||
return state().then((x) => x.warnings)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
export async function getGlobal() {
|
||||
return global()
|
||||
}
|
||||
@@ -1706,5 +1865,3 @@ export namespace Config {
|
||||
return state().then((x) => x.directories)
|
||||
}
|
||||
}
|
||||
Filesystem.write
|
||||
Filesystem.write
|
||||
|
||||
@@ -15,6 +15,7 @@ import { FormatError } from "./cli/error"
|
||||
import { ServeCommand } from "./cli/cmd/serve"
|
||||
import { WorkspaceServeCommand } from "./cli/cmd/workspace-serve"
|
||||
import { Filesystem } from "./util/filesystem"
|
||||
import { ConfigCommand as ConfigCLICommand } from "./cli/cmd/config" // kilocode_change
|
||||
import { DebugCommand } from "./cli/cmd/debug"
|
||||
import { StatsCommand } from "./cli/cmd/stats"
|
||||
import { McpCommand } from "./cli/cmd/mcp"
|
||||
@@ -195,6 +196,7 @@ let cli = yargs(hideBin(process.argv))
|
||||
.command(SessionCommand)
|
||||
.command(RemoteCommand) // kilocode_change
|
||||
.command(DbCommand)
|
||||
.command(ConfigCLICommand) // kilocode_change
|
||||
|
||||
if (Installation.isLocal()) {
|
||||
cli = cli.command(WorkspaceServeCommand)
|
||||
|
||||
@@ -15,7 +15,7 @@ export const BUILTIN_SKILLS: BuiltinSkill[] = [
|
||||
{
|
||||
name: "kilo-config",
|
||||
description:
|
||||
"Guide for configuring Kilo CLI: commands, agents, MCP servers, skills, permissions, instructions, plugins, providers, all kilo.json fields, and TUI settings (themes, appearance, keybinds, ctrl+p commands). Use when the user asks about configuring, customizing, or changing settings in Kilo.",
|
||||
"Guide for configuring Kilo CLI and locating config, command, agent, and skill paths (global, project, legacy), plus MCP servers, permissions, instructions, plugins, providers, kilo.json fields, and TUI settings (themes, appearance, keybinds, ctrl+p commands). Use when the user asks about configuring Kilo, where it loads things from, or how to change settings.",
|
||||
content: KILO_CONFIG,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
All config lives in `kilo.json` (or `kilo.jsonc`). Precedence low-to-high: remote well-known, global (`~/.config/kilo/kilo.json`), env `KILO_CONFIG`, project `./kilo.json`, `.kilo/kilo.json`, `KILO_CONFIG_CONTENT`, managed (see Config File Locations). Deep-merged; later wins.
|
||||
|
||||
This also covers where Kilo looks for config files, commands, agents, and skills across project, global, and legacy paths such as `.kilo/`, `.kilocode/`, `.opencode/`, and `~/.config/kilo/`.
|
||||
|
||||
## Commands (`.kilo/command/*.md`)
|
||||
|
||||
Markdown files with YAML frontmatter. The filename (minus `.md`) becomes the command name invoked via `/name`.
|
||||
Markdown files with YAML frontmatter. The filename (minus `.md`) becomes the command name invoked via `/name`. Commands can live in `.kilo/`, `.kilocode/`, `.opencode/`, and global config roots, with both `command/` and `commands/` directory names supported. See Config File Locations for the full search order.
|
||||
|
||||
```yaml
|
||||
---
|
||||
@@ -20,8 +22,28 @@ Reference files with @file and shell output with !`cmd`.
|
||||
|
||||
Template variables: `$1`-`$N` (positional args), `$ARGUMENTS` (full string), `@file` (file contents), `` !`cmd` `` (shell output).
|
||||
|
||||
### Finding a named command
|
||||
|
||||
When asked where `/name` lives, do not search only the repo root. Search these roots explicitly, and use an explicit search `path` for each one:
|
||||
|
||||
1. `~/.config/kilo/`
|
||||
2. `~/.kilo/`
|
||||
3. `~/.kilocode/`
|
||||
4. `~/.opencode/`
|
||||
5. The `KILO_CONFIG_DIR` directory (if the env var is set)
|
||||
6. project `.kilo/`, `.kilocode/`, and `.opencode/` directories from the current working directory up to the worktree root
|
||||
|
||||
Use exact patterns first:
|
||||
|
||||
- `**/command/<name>.md`
|
||||
- `**/commands/<name>.md`
|
||||
|
||||
If found, return the full path. If not found in those roots, explain that the command is not present in the loaded config paths.
|
||||
|
||||
## Agents (`.kilo/agent/*.md`)
|
||||
|
||||
Also loaded from `.kilocode/` and `.opencode/` directories (legacy), and plural `agents/` variants.
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: When to use this agent
|
||||
@@ -41,6 +63,10 @@ System prompt for this agent.
|
||||
|
||||
`mode` values: `primary` = selectable as main agent, `subagent` = only via Task tool, `all` = both.
|
||||
|
||||
## Workflows (legacy)
|
||||
|
||||
Markdown files in `.kilo/workflows/` or `.kilocode/workflows/` (project-level) and `~/.kilo/workflows/` or `~/.kilocode/workflows/` (global). These are automatically converted to commands at startup. The filename (minus `.md`) becomes the command name. Project workflows override global ones with the same name.
|
||||
|
||||
## Permissions
|
||||
|
||||
Scalar form applies to all patterns. Object form maps glob patterns to actions. Evaluated top-to-bottom; first match wins.
|
||||
@@ -127,7 +153,7 @@ Additional skill directories and remote URLs:
|
||||
}
|
||||
```
|
||||
|
||||
Skills are markdown files at `skills/<name>/SKILL.md` with `name` and `description` in frontmatter.
|
||||
Skills are markdown files at `skills/<name>/SKILL.md` (or `skill/<name>/SKILL.md`) with `name` and `description` in frontmatter. Discovered inside `.kilo/`, `.kilocode/`, and `.opencode/` directories.
|
||||
|
||||
## Other Top-Level Fields
|
||||
|
||||
@@ -200,12 +226,49 @@ Toggle notifications, Toggle animations, Toggle diff wrapping, Toggle sidebar (`
|
||||
|
||||
## Config File Locations
|
||||
|
||||
| Scope | Path |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Project | `./kilo.json`, `./.kilo/kilo.json` |
|
||||
| Global | `~/.config/kilo/kilo.json` |
|
||||
| Managed | Linux: `/etc/kilo/kilo.json`, macOS: `/Library/Application Support/kilo/kilo.json`, Windows: `%ProgramData%\kilo\kilo.json` (enterprise, highest priority) |
|
||||
| Commands | `.kilo/command/*.md` (project), `~/.config/kilo/command/*.md` (global) |
|
||||
| Agents | `.kilo/agent/*.md` (project), `~/.config/kilo/agent/*.md` (global) |
|
||||
| Skills | `.kilo/skill/*/SKILL.md`, `.kilo/skills/*/SKILL.md` |
|
||||
| Instructions | `AGENTS.md`, `.kilo/instructions.md`, glob patterns from `instructions` |
|
||||
### Config files (kilo.json)
|
||||
|
||||
| Scope | Path |
|
||||
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Project | `./kilo.json`, `./kilo.jsonc`, `./opencode.json` (legacy), `./opencode.jsonc` (legacy) |
|
||||
| Global | `~/.config/kilo/kilo.json`, `~/.config/kilo/kilo.jsonc`, `~/.config/kilo/opencode.json` (legacy), `~/.config/kilo/opencode.jsonc` (legacy), `~/.config/kilo/config.json` (legacy) |
|
||||
| Managed | Linux: `/etc/kilo/`, macOS: `/Library/Application Support/kilo/`, Windows: `%ProgramData%\kilo\` — loads `kilo.json`, `kilo.jsonc`, `opencode.json`, `opencode.jsonc` (enterprise, highest priority) |
|
||||
|
||||
Each config directory (`.kilo/`, `.kilocode/`, `.opencode/`) can also contain `kilo.json`, `kilo.jsonc`, `opencode.json`, or `opencode.jsonc`.
|
||||
|
||||
### Config directories
|
||||
|
||||
Three directory names are scanned: `.kilo` (modern), `.kilocode` (legacy), `.opencode` (legacy). All three are checked at each level:
|
||||
|
||||
- **Project**: walks up from CWD to the git worktree root, checking for all three at each directory level
|
||||
- **Home**: `~/.kilo/`, `~/.kilocode/`, `~/.opencode/`
|
||||
- **XDG global**: `~/.config/kilo/` (always loaded, lowest file-based precedence)
|
||||
|
||||
### Commands, agents, modes, plugins
|
||||
|
||||
Glob patterns run inside every discovered config directory (including legacy):
|
||||
|
||||
| Type | Pattern |
|
||||
| ------- | ---------------------------- |
|
||||
| Command | `{command,commands}/**/*.md` |
|
||||
| Agent | `{agent,agents}/**/*.md` |
|
||||
| Mode | `{mode,modes}/*.md` |
|
||||
| Plugin | `{plugin,plugins}/*.{ts,js}` |
|
||||
|
||||
Example: `~/.config/kilo/command/*.md` (modern global), `~/.kilocode/command/*.md` (legacy global), `.opencode/commands/*.md` (legacy project) all load commands.
|
||||
|
||||
### Skills and instructions
|
||||
|
||||
| Scope | Path |
|
||||
| ------------ | -------------------------------------------------------------------------------------- |
|
||||
| Skills | `{skill,skills}/<name>/SKILL.md` inside any config directory |
|
||||
| Instructions | `AGENTS.md`, `CLAUDE.md`, `CONTEXT.md`, glob patterns from `instructions` config field |
|
||||
|
||||
### Environment variable overrides
|
||||
|
||||
| Variable | Description |
|
||||
| ----------------------------- | ---------------------------------------------------------------- |
|
||||
| `KILO_CONFIG` | Path to an additional config file (loaded after global) |
|
||||
| `KILO_CONFIG_DIR` | Path to an additional config directory (appended to search list) |
|
||||
| `KILO_CONFIG_CONTENT` | Inline JSON config string (high precedence, after project dirs) |
|
||||
| `KILO_DISABLE_PROJECT_CONFIG` | Skip all project-level config (files and directories) |
|
||||
|
||||
@@ -62,6 +62,29 @@ export const ConfigRoutes = lazy(() =>
|
||||
return c.json(config)
|
||||
},
|
||||
)
|
||||
// kilocode_change start
|
||||
.get(
|
||||
"/warnings",
|
||||
describeRoute({
|
||||
summary: "Get config warnings",
|
||||
description: "Get warnings generated during config loading (e.g., invalid JSON, schema errors).",
|
||||
operationId: "config.warnings",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Config warnings",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Config.Warning.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await Config.warnings())
|
||||
},
|
||||
)
|
||||
// kilocode_change end
|
||||
.get(
|
||||
"/providers",
|
||||
describeRoute({
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
afterEach(async () => {
|
||||
await Instance.disposeAll()
|
||||
Config.global.reset()
|
||||
})
|
||||
|
||||
describe("config resilience", () => {
|
||||
test("skips invalid agent markdown configs", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Filesystem.write(
|
||||
path.join(dir, ".kilo", "agent", "skip.md"),
|
||||
`---
|
||||
mode: "banana"
|
||||
---
|
||||
Broken agent prompt`,
|
||||
)
|
||||
await Filesystem.write(
|
||||
path.join(dir, ".kilo", "agent", "keep.md"),
|
||||
`---
|
||||
model: test/model
|
||||
---
|
||||
Valid agent prompt`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const cfg = await Config.get()
|
||||
|
||||
expect(cfg.agent?.["skip"]).toBeUndefined()
|
||||
expect(cfg.agent?.["keep"]).toMatchObject({
|
||||
name: "keep",
|
||||
model: "test/model",
|
||||
prompt: "Valid agent prompt",
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("reports a warning for invalid agent markdown configs", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Filesystem.write(
|
||||
path.join(dir, ".kilo", "agent", "skip.md"),
|
||||
`---
|
||||
mode: "banana"
|
||||
---
|
||||
Broken agent prompt`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Config.get()
|
||||
const warns = await Config.warnings()
|
||||
|
||||
expect(warns.some((w) => w.path.includes("skip.md") && w.message.includes("mode"))).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("skips invalid command markdown configs", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Filesystem.write(
|
||||
path.join(dir, ".kilo", "command", "skip.md"),
|
||||
`---
|
||||
subtask: "banana"
|
||||
---
|
||||
Broken command template`,
|
||||
)
|
||||
await Filesystem.write(
|
||||
path.join(dir, ".kilo", "command", "keep.md"),
|
||||
`---
|
||||
description: Valid command
|
||||
---
|
||||
Valid command template`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const cfg = await Config.get()
|
||||
|
||||
expect(cfg.command?.["skip"]).toBeUndefined()
|
||||
expect(cfg.command?.["keep"]).toEqual({
|
||||
description: "Valid command",
|
||||
template: "Valid command template",
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("reports a warning for invalid command markdown configs", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Filesystem.write(
|
||||
path.join(dir, ".kilo", "command", "skip.md"),
|
||||
`---
|
||||
subtask: "banana"
|
||||
---
|
||||
Broken command template`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Config.get()
|
||||
const warns = await Config.warnings()
|
||||
|
||||
expect(warns.some((w) => w.path.includes("skip.md") && w.message.includes("subtask"))).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("collects warnings for invalid agent markdown configs", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Filesystem.write(
|
||||
path.join(dir, ".kilo", "agent", "broken.md"),
|
||||
`---
|
||||
mode: "banana"
|
||||
---
|
||||
Broken agent`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Config.get()
|
||||
const warns = await Config.warnings()
|
||||
|
||||
expect(warns.some((w) => w.path.includes("broken.md") && w.message.includes("invalid"))).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("collects warnings for invalid command markdown configs", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Filesystem.write(
|
||||
path.join(dir, ".kilo", "command", "broken.md"),
|
||||
`---
|
||||
subtask: "banana"
|
||||
---
|
||||
Broken command`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Config.get()
|
||||
const warns = await Config.warnings()
|
||||
|
||||
expect(warns.some((w) => w.path.includes("broken.md") && w.message.includes("invalid"))).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("collects warnings for invalid JSON in .kilo directory config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Filesystem.write(path.join(dir, ".kilo", "kilo.json"), "{ not valid json !!!")
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const cfg = await Config.get()
|
||||
const warns = await Config.warnings()
|
||||
|
||||
// Config loading should not crash
|
||||
expect(cfg).toBeDefined()
|
||||
// Warning should reference the bad file
|
||||
expect(warns.some((w) => w.path.includes("kilo.json") && w.message.includes("not valid JSON"))).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("collects warnings for invalid schema in .kilo directory config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Filesystem.write(path.join(dir, ".kilo", "kilo.json"), JSON.stringify({ unknownField: true }))
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const cfg = await Config.get()
|
||||
const warns = await Config.warnings()
|
||||
|
||||
expect(cfg).toBeDefined()
|
||||
expect(warns.some((w) => w.path.includes("kilo.json") && w.message.includes("invalid"))).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("returns empty warnings when config is valid", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
config: { model: "test/model" },
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Config.get()
|
||||
const warns = await Config.warnings()
|
||||
|
||||
expect(warns).toEqual([])
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -109,4 +109,36 @@ Use this skill.
|
||||
process.env.KILO_TEST_HOME = home
|
||||
}
|
||||
})
|
||||
|
||||
test("built-in kilo-config includes named command lookup guidance", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
const home = process.env.KILO_TEST_HOME
|
||||
process.env.KILO_TEST_HOME = tmp.path
|
||||
|
||||
try {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const tool = await SkillTool.init()
|
||||
const ctx: Tool.Context = {
|
||||
...baseCtx,
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
const result = await tool.execute({ name: "kilo-config" }, ctx)
|
||||
|
||||
expect(tool.description).toContain("where it loads things from")
|
||||
expect(result.metadata.dir).toBe("builtin")
|
||||
expect(result.output).toContain("### Finding a named command")
|
||||
expect(result.output).toContain("`~/.config/kilo/`")
|
||||
expect(result.output).toContain("`~/.kilocode/`")
|
||||
expect(result.output).toContain("`**/command/<name>.md`")
|
||||
expect(result.output).toContain("explicit search `path`")
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
process.env.KILO_TEST_HOME = home
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
ConfigProvidersResponses,
|
||||
ConfigUpdateErrors,
|
||||
ConfigUpdateResponses,
|
||||
ConfigWarningsResponses,
|
||||
EnhancePromptEnhanceErrors,
|
||||
EnhancePromptEnhanceResponses,
|
||||
EventSubscribeResponses,
|
||||
@@ -843,6 +844,36 @@ export class Config2 extends HeyApiClient {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config warnings
|
||||
*
|
||||
* Get warnings generated during config loading (e.g., invalid JSON, schema errors).
|
||||
*/
|
||||
public warnings<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).get<ConfigWarningsResponses, unknown, ThrowOnError>({
|
||||
url: "/config/warnings",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List config providers
|
||||
*
|
||||
|
||||
@@ -2462,6 +2462,29 @@ export type ConfigUpdateResponses = {
|
||||
|
||||
export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses]
|
||||
|
||||
export type ConfigWarningsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/config/warnings"
|
||||
}
|
||||
|
||||
export type ConfigWarningsResponses = {
|
||||
/**
|
||||
* Config warnings
|
||||
*/
|
||||
200: Array<{
|
||||
path: string
|
||||
message: string
|
||||
detail?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export type ConfigWarningsResponse = ConfigWarningsResponses[keyof ConfigWarningsResponses]
|
||||
|
||||
export type ConfigProvidersData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
+441
-1719
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user