fix(cli): stop loading .opencode config directories

This commit is contained in:
marius-kilocode
2026-06-24 17:01:32 +02:00
parent 117a0d6233
commit ef9b1dcec0
42 changed files with 245 additions and 203 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
"@kilocode/cli": patch
---
Prefer project `.kilo` configuration over legacy `.kilocode` and `.opencode` directories when entries conflict.
Stop loading `.opencode` config directories and use `.kilo` instead, while retaining `.kilocode` as a legacy fallback.
@@ -58,7 +58,7 @@ Config files live in the same locations as the rest of your CLI configuration
Drop TypeScript or JavaScript files into a `plugin/` or `plugins/` folder inside any config directory:
- Global: `~/.config/kilo/plugin/`
- Project: `.kilo/plugin/`, `.kilocode/plugin/`, or `.opencode/plugin/`
- Project: `.kilo/plugin/` or legacy `.kilocode/plugin/`
Every `.ts` or `.js` file in those directories is auto-registered at startup — no need to list them in the config file.
@@ -86,7 +86,7 @@ kilo plugin my-plugin --global
kilo plugin my-plugin --force
```
The command resolves the package, reads its `package.json` for plugin entrypoints, and writes the entry into the appropriate config file (currently `.opencode/opencode.jsonc` / `.opencode/tui.jsonc` for local installs, or `~/.config/kilo/opencode.jsonc` / `~/.config/kilo/tui.jsonc` for `--global`) while preserving JSONC comments.
The command resolves the package, reads its `package.json` for plugin entrypoints, and writes the entry into the appropriate config file (`.kilo/opencode.jsonc` / `.kilo/tui.jsonc` for local installs, or `~/.config/kilo/opencode.jsonc` / `~/.config/kilo/tui.jsonc` for `--global`) while preserving JSONC comments.
### How plugins are installed
@@ -446,7 +446,7 @@ If a custom tool uses the same name as a built-in tool, **the custom tool wins**
### Alternative: standalone tool files
For tools that don't need the full plugin context, drop them in a `tool/` or `tools/` folder inside any config directory — for example `.kilo/tool/database.ts` or `~/.config/kilo/tool/database.ts`. The filename becomes the tool name, and each file exports a `tool()` definition directly. The layout is identical to the [OpenCode custom tools guide](https://opencode.ai/docs/custom-tools); substitute `.kilo/` (or `.kilocode/` / `.opencode/`) for `.opencode/`.
For tools that don't need the full plugin context, drop them in a `tool/` or `tools/` folder inside any config directory — for example `.kilo/tool/database.ts` or `~/.config/kilo/tool/database.ts`. The filename becomes the tool name, and each file exports a `tool()` definition directly. The layout is identical to the [OpenCode custom tools guide](https://opencode.ai/docs/custom-tools); use `.kilo/`, or legacy `.kilocode/`, instead of `.opencode/`.
---
@@ -335,8 +335,8 @@ The Kilo CLI is a fork of [OpenCode](https://opencode.ai) and supports the same
| Scope | Path |
|---|---|
| **Global** | `~/.config/kilo/opencode.json` or `opencode.jsonc` (Windows: config dir may vary; same filenames) |
| **Project** | `./opencode.json` or `./.opencode/` in project root |
| **Global** | `~/.config/kilo/kilo.json[c]` or legacy `opencode.json[c]` (Windows config dir may vary) |
| **Project** | `./kilo.json[c]`, legacy `./opencode.json[c]`, or config inside `./.kilo/` (legacy `./.kilocode/` is also read) |
Project-level configuration takes precedence over global settings.
@@ -262,7 +262,7 @@ Later sources override earlier values during instance config load:
| 11 | macOS managed preferences |
| 12 | Runtime flag-derived permission, tool, compaction, and plugin behavior |
Global config files load from `${Global.Path.config}`. Project updates prefer existing config files found in ancestor `.kilo`, `.kilocode`, or `.opencode` directories, then existing project root config files, then create `.kilo/kilo.json`. Global indexing settings can carry provider and storage defaults, but global `indexing.enabled` is stripped so project enablement remains local in effective instance config.
Global config files load from `${Global.Path.config}`. Project updates prefer existing config files found in ancestor `.kilo` or legacy `.kilocode` directories, then existing project root config files, then create `.kilo/kilo.json`. Global indexing settings can carry provider and storage defaults, but global `indexing.enabled` is stripped so project enablement remains local in effective instance config.
Signed-in organization modes become normal agent configuration during load. They override migrated legacy modes and remain overridable by later config sources in table.
@@ -76,7 +76,7 @@ Create `.md` files in any of these directories:
```
.kilo/agents/my-agent.md
.kilo/agent/my-agent.md
.opencode/agents/my-agent.md
.kilocode/agents/my-agent.md
```
For global agents, place files in your global config directory:
@@ -191,7 +191,7 @@ Agent configurations merge from lowest to highest priority:
1. Built-in (native) agent defaults
2. Global config (`~/.config/kilo/kilo.jsonc`)
3. Project config (`kilo.jsonc` at project root)
4. `.kilo/` / `.opencode/` directory configs and agent `.md` files
4. `.kilo/` / legacy `.kilocode/` directory configs and agent `.md` files
5. Environment variable overrides (`KILO_CONFIG_CONTENT`)
When the same agent name appears at multiple levels, properties are merged (not replaced wholesale), so you can override just a model or temperature without redefining the entire agent.
@@ -312,7 +312,7 @@ Create `.md` files in any of these directories:
```
.kilo/agents/my-agent.md
.kilo/agent/my-agent.md
.opencode/agents/my-agent.md
.kilocode/agents/my-agent.md
```
For global agents, place files in your global config directory:
@@ -427,7 +427,7 @@ Agent configurations merge from lowest to highest priority:
1. Built-in (native) agent defaults
2. Global config (`~/.config/kilo/kilo.jsonc`)
3. Project config (`kilo.jsonc` at project root)
4. `.kilo/` / `.opencode/` directory configs and agent `.md` files
4. `.kilo/` / legacy `.kilocode/` directory configs and agent `.md` files
5. Environment variable overrides (`KILO_CONFIG_CONTENT`)
When the same agent name appears at multiple levels, properties are merged (not replaced wholesale), so you can override just a model or temperature without redefining the entire agent.
@@ -579,7 +579,7 @@ When importing modes, you can change the slug in the exported YAML file before i
Custom agents are defined as Markdown files with optional YAML frontmatter. You can place them in:
- **Project agents:** `.kilo/agents/*.md` (or `.opencode/agents/*.md`)
- **Project agents:** `.kilo/agents/*.md` (or legacy `.kilocode/agents/*.md`)
- **Global agents:** `~/.config/kilo/agents/*.md`
### Agent File Format
@@ -1256,7 +1256,7 @@ customModes:
### Common Issues
- **Agent not appearing:** Ensure the `.md` file is in a recognized directory (`.kilo/agents/`, `.kilo/agent/`, `.opencode/agents/`). Check that the `mode` property is `primary` or `all` if you expect it in the agent picker.
- **Agent not appearing:** Ensure the `.md` file is in a recognized directory (`.kilo/agents/`, `.kilo/agent/`, `.kilocode/agents/`). Check that the `mode` property is `primary` or `all` if you expect it in the agent picker.
- **Permission errors:** Permission rules are evaluated last-match-wins. If an agent can't use a tool you expect, check that an `allow` rule appears after any `deny` rules for that permission.
- **YAML frontmatter parse errors:** Ensure the frontmatter block starts and ends with `---` on its own line. Validate that YAML keys match expected property names (e.g., `top_p` not `topP`).
- **Agent overrides not working:** Config merges from global to project level. If a global config sets a property, your project config can override it, but both must use the same agent name.
@@ -1273,7 +1273,7 @@ customModes:
### Common Issues
- **Agent not appearing:** Ensure the `.md` file is in a recognized directory (`.kilo/agents/`, `.kilo/agent/`, `.opencode/agents/`). Check that the `mode` property is `primary` or `all` if you expect it in the agent picker.
- **Agent not appearing:** Ensure the `.md` file is in a recognized directory (`.kilo/agents/`, `.kilo/agent/`, `.kilocode/agents/`). Check that the `mode` property is `primary` or `all` if you expect it in the agent picker.
- **Permission errors:** Permission rules are evaluated last-match-wins. If an agent can't use a tool you expect, check that an `allow` rule appears after any `deny` rules for that permission.
- **YAML frontmatter parse errors:** Ensure the frontmatter block starts and ends with `---` on its own line. Validate that YAML keys match expected property names (e.g., `top_p` not `topP`).
- **Agent overrides not working:** Config merges from global to project level. If a global config sets a property, your project config can override it, but both must use the same agent name.
+5 -8
View File
@@ -195,8 +195,8 @@ npm plugins can declare a version compatibility range in `package.json` using th
- Alias: `kilo plug <module>`.
- `-g` / `--global` writes into the global config dir.
- Local installs resolve target dir inside `patchPluginConfig`.
- For local scope, path is `<worktree>/.opencode` only when VCS is git and `worktree !== "/"`; otherwise `<directory>/.opencode`.
- Root-worktree fallback (`worktree === "/"` uses `<directory>/.opencode`) is covered by regression tests.
- For local scope, path is `<worktree>/.kilo` only when VCS is git and `worktree !== "/"`; otherwise `<directory>/.kilo`.
- Root-worktree fallback (`worktree === "/"` uses `<directory>/.kilo`) is covered by regression tests.
- `patchPluginConfig` applies all detected targets (`server` and/or `tui`) in one call.
- `patchPluginConfig` returns structured result unions (`ok`, `code`, fields by error kind) instead of custom thrown errors.
- `patchPluginConfig` serializes per-target config writes with `Flock.acquire(...)`.
@@ -213,7 +213,7 @@ npm plugins can declare a version compatibility range in `package.json` using th
- There is no uninstall, list, or update CLI command for external plugins.
- Local file plugins are configured directly in `tui.json`.
When `plugin` entries exist in a writable `.opencode` dir or `KILO_CONFIG_DIR`, Kilo installs `@kilocode/plugin` into that dir and writes:
When `plugin` entries exist in a writable `.kilo` or legacy `.kilocode` directory, or `KILO_CONFIG_DIR`, Kilo installs `@kilocode/plugin` into that directory and writes:
- `package.json`
- `bun.lock`
@@ -413,7 +413,7 @@ Theme install behavior:
- If the theme name already exists, install is skipped unless plugin metadata state is `updated`.
- On `updated`, host skips rewrite when tracked `mtime`/`size` is unchanged.
- When a theme already exists and state is not `updated`, host can still persist theme metadata when destination already exists.
- Local plugins persist installed themes under the local `.opencode/themes` area near the plugin config source.
- Local plugins persist installed themes under `.kilo/themes`, or under `themes` beside a config source in `.kilo` or legacy `.kilocode`.
- Global plugins persist installed themes under the global `themes` dir.
- Invalid or unreadable theme files are ignored.
@@ -538,7 +538,4 @@ The plugin manager is exposed as a command with title `Plugins` and value `plugi
## Current in-repo examples
- Local smoke plugin: `.opencode/plugins/tui-smoke.tsx`
- Local vim plugin: `.opencode/plugins/tui-vim.tsx`
- Local smoke config: `.opencode/tui.json`
- Local smoke theme: `.opencode/plugins/smoke-theme.json`
The upstream `.opencode` smoke fixtures remain in the repository for OpenCode compatibility, but Kilo does not auto-load them. Kilo-local examples must live under `.kilo` or legacy `.kilocode`.
+17 -16
View File
@@ -397,25 +397,26 @@ export const McpLogoutCommand = effectCmd({
})
async function resolveConfigPath(baseDir: string, global = false) {
// kilocode_change start - prefer kilo.json/.kilo over opencode.json/.opencode
// Check for existing config files (prefer .jsonc over .json, check .kilo/ and .opencode/ subdirectory too)
const candidates = [
path.join(baseDir, "kilo.json"),
// kilocode_change start - prefer supported Kilo config directories over root files
const roots = [
path.join(baseDir, "kilo.jsonc"),
path.join(baseDir, "opencode.json"),
path.join(baseDir, "kilo.json"),
path.join(baseDir, "opencode.jsonc"),
path.join(baseDir, "opencode.json"),
]
if (!global) {
candidates.push(
path.join(baseDir, ".kilo", "kilo.json"),
path.join(baseDir, ".kilo", "kilo.jsonc"),
path.join(baseDir, ".kilo", "opencode.json"),
path.join(baseDir, ".kilo", "opencode.jsonc"),
path.join(baseDir, ".opencode", "opencode.json"),
path.join(baseDir, ".opencode", "opencode.jsonc"),
)
}
const candidates = global
? roots
: [
path.join(baseDir, ".kilo", "kilo.jsonc"),
path.join(baseDir, ".kilo", "kilo.json"),
path.join(baseDir, ".kilo", "opencode.jsonc"),
path.join(baseDir, ".kilo", "opencode.json"),
path.join(baseDir, ".kilocode", "kilo.jsonc"),
path.join(baseDir, ".kilocode", "kilo.json"),
path.join(baseDir, ".kilocode", "opencode.jsonc"),
path.join(baseDir, ".kilocode", "opencode.json"),
...roots,
]
for (const candidate of candidates) {
if (await Filesystem.exists(candidate)) {
@@ -191,8 +191,10 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
acc.plugin_origins = plugins
})
// Every config dir we may read from: global config dir, any `.opencode`
// kilocode_change start - discover canonical and legacy Kilo config directories
// Every config dir we may read from: global config, .kilo and legacy .kilocode
// folders between cwd and home, and KILO_CONFIG_DIR.
// kilocode_change end
const directories = yield* ConfigPaths.directories(ctx.directory)
yield* Effect.promise(() => migrateTuiConfig({ directories, cwd: ctx.directory }))
@@ -220,18 +222,16 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
yield* mergeFile(acc, file)
}
// 4. `.opencode` directories (and KILO_CONFIG_DIR) discovered while
// walking up the tree. Also returned below so callers can install plugin
// dependencies from each location.
// kilocode_change start - also load tui.json from .kilo/.kilocode
// kilocode_change start - load tui.json from supported Kilo config directories
// 4. `.kilo` and legacy `.kilocode` directories (and KILO_CONFIG_DIR)
// discovered while walking up the tree. Also returned below so callers can
// install plugin dependencies from each location.
const dirs = unique(directories).filter(
(dir) =>
dir.endsWith(".kilo") || dir.endsWith(".kilocode") || dir.endsWith(".opencode") || dir === Flag.KILO_CONFIG_DIR,
(dir) => dir.endsWith(".kilo") || dir.endsWith(".kilocode") || dir === Flag.KILO_CONFIG_DIR,
)
// kilocode_change end
for (const dir of dirs) {
// if (!dir.endsWith(".opencode") && dir !== Flag.KILO_CONFIG_DIR) continue // kilocode_change
for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
yield* mergeFile(acc, file)
}
@@ -508,7 +508,7 @@ async function getCustomThemes() {
Global.Path.config,
...(await Array.fromAsync(
Filesystem.up({
targets: [".kilo", ".opencode"], // kilocode_change
targets: [".kilocode", ".kilo"], // kilocode_change
start: process.cwd(),
}),
)),
@@ -135,10 +135,12 @@ export function Tips(props: { api: TuiPluginApi; connected?: boolean }) {
}
const tip = createMemo(() => {
if (props.connected === false) return NO_MODELS_TIP
const tips = KILO_TIPS.flatMap((item) => { // kilocode_change
// kilocode_change start
const tips = KILO_TIPS.flatMap((item) => {
const value = typeof item === "string" ? item : item(shortcuts)
return value ? [value] : []
})
// kilocode_change end
return tips[Math.floor(tipOffset * tips.length)] ?? NO_MODELS_TIP
}, NO_MODELS_TIP)
// Solid can expose a memo's initial value while a pure computation is pending.
@@ -219,10 +221,10 @@ const TIPS: Tip[] = [
"Override any keybind in {highlight}tui.json{/highlight} via the {highlight}keybinds{/highlight} section",
"Set any keybind to {highlight}none{/highlight} to disable it completely",
"Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section",
"Add {highlight}.md{/highlight} files to {highlight}.opencode/commands/{/highlight} to define reusable custom prompts",
"Add {highlight}.md{/highlight} files to {highlight}.kilo/commands/{/highlight} to define reusable custom prompts", // kilocode_change
"Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input",
"Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight})",
"Add {highlight}.md{/highlight} files to {highlight}.opencode/agents/{/highlight} for specialized AI personas",
"Add {highlight}.md{/highlight} files to {highlight}.kilo/agents/{/highlight} for specialized AI personas", // kilocode_change
"Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools",
'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions',
'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands',
@@ -231,9 +233,9 @@ const TIPS: Tip[] = [
'Set {highlight}"formatter": false{/highlight} in config to disable formatters enabled by another config layer',
"Define custom formatter commands with file extensions in config",
'Set {highlight}"lsp": true{/highlight} in config to enable built-in LSP servers for code analysis',
"Create {highlight}.ts{/highlight} files in {highlight}.opencode/tools/{/highlight} to define new LLM tools",
"Create {highlight}.ts{/highlight} files in {highlight}.kilo/tools/{/highlight} to define new LLM tools", // kilocode_change
"Tool definitions can invoke scripts written in Python, Go, etc",
"Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugins/{/highlight} for event hooks",
"Add {highlight}.ts{/highlight} files to {highlight}.kilo/plugins/{/highlight} for event hooks", // kilocode_change
"Use plugins to send OS notifications when sessions complete",
"Create a plugin to prevent OpenCode from reading sensitive files",
"Use {highlight}opencode run{/highlight} for non-interactive scripting",
@@ -250,7 +252,7 @@ const TIPS: Tip[] = [
"Comment {highlight}/opencode fix this{/highlight} on issues to auto-create PRs",
"Comment {highlight}/oc{/highlight} on PR code lines for targeted code reviews",
'Use {highlight}"theme": "system"{/highlight} to match your terminal\'s colors',
"Create JSON theme files in {highlight}.opencode/themes/{/highlight} directory",
"Create JSON theme files in {highlight}.kilo/themes/{/highlight} directory", // kilocode_change
"Themes support dark/light variants for both modes",
"Use numeric xterm color codes 0-255 in custom theme JSON",
"Use {highlight}{env:VAR_NAME}{/highlight} syntax to reference environment variables in config",
@@ -254,10 +254,13 @@ function createThemeInstaller(
const src = Filesystem.resolveFilePath(root, file)
const name = path.basename(src, path.extname(src))
const source_dir = path.dirname(meta.source)
// kilocode_change start - install local themes into supported Kilo config directories
const base = path.basename(source_dir)
const local_dir =
path.basename(source_dir) === ".opencode"
base === ".kilo" || base === ".kilocode"
? path.join(source_dir, "themes")
: path.join(source_dir, ".opencode", "themes")
: path.join(source_dir, ".kilo", "themes")
// kilocode_change end
const dest_dir = meta.scope === "local" ? local_dir : path.join(Global.Path.config, "themes")
const dest = path.join(dest_dir, `${name}.json`)
const stat = await Filesystem.statAsync(src)
@@ -843,7 +846,7 @@ function defaultPluginOrigin(state: RuntimeState, spec: string): ConfigPlugin.Or
return {
spec,
scope: "local",
source: state.api.state.path.config || path.join(state.directory, ".opencode", "tui.json"),
source: state.api.state.path.config || path.join(state.directory, ".kilo", "tui.json"), // kilocode_change
}
}
+2 -2
View File
@@ -865,7 +865,7 @@ export const layer = Layer.effect(
const directories = yield* ConfigPaths.directories(ctx.directory, ctx.worktree)
const primary = Flag.KILO_DISABLE_PROJECT_CONFIG
? []
: yield* primaryPaths(ctx.directory, ctx.worktree, [".opencode", ".kilocode", ".kilo"])
: yield* primaryPaths(ctx.directory, ctx.worktree, [".kilocode", ".kilo"])
// Load primary fallbacks before active-worktree config, then track them as local.
directories.splice(1, 0, ...primary)
const primarySet = new Set(primary)
@@ -934,7 +934,7 @@ export const layer = Layer.effect(
result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir, warnings)))
result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir, warnings)))
// kilocode_change end
// Auto-discovered plugins under `.opencode/plugin(s)` are already local files, so ConfigPlugin.load
// kilocode_change - Auto-discovered plugins under config directories are already local files, so ConfigPlugin.load
// returns normalized Specs and we only need to attach origin metadata here.
const list = yield* Effect.promise(() => ConfigPlugin.load(dir))
yield* mergePluginOrigins(dir, list, scope) // kilocode_change
+2 -2
View File
@@ -26,13 +26,13 @@ export const directories = Effect.fn("ConfigPaths.directories")(function* (direc
Global.Path.config,
...(!Flag.KILO_DISABLE_PROJECT_CONFIG
? yield* afs.up({
targets: [".opencode", ".kilocode", ".kilo"], // kilocode_change
targets: [".kilocode", ".kilo"], // kilocode_change
start: directory,
stop: worktree,
})
: []),
...(yield* afs.up({
targets: [".opencode", ".kilocode", ".kilo"], // kilocode_change
targets: [".kilocode", ".kilo"], // kilocode_change
start: Global.Path.home,
stop: Global.Path.home,
})),
+1 -1
View File
@@ -441,7 +441,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | ChildPro
const root: Node = { name: "", children: new Map() }
for (const file of list) {
if (file.includes(".kilo") || file.includes(".opencode")) continue // kilocode_change
if (file.includes(".kilo") || file.includes(".kilocode")) continue // kilocode_change
const parts = file.split(path.sep)
if (parts.length < 2) continue
let node = root
@@ -43,12 +43,9 @@ export namespace KilocodeConfig {
/** All config file names in precedence order (kilo + opencode). */
export const ALL_CONFIG_FILES = ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json"] as const
/** Directory suffixes that Kilo recognizes in addition to .opencode. */
/** Config directory suffixes in update-target preference order. */
export const KILO_DIR_SUFFIXES = [".kilo", ".kilocode"] as const
/** All config directory suffixes Kilo can update, including upstream .opencode. */
export const ALL_CONFIG_DIR_SUFFIXES = [".kilo", ".kilocode", ".opencode"] as const
/** Path patterns for resolving kilo agent names from file paths. */
export const AGENT_PATTERNS = ["/.kilo/agent/", "/.kilo/agents/", "/.kilocode/agent/", "/.kilocode/agents/"] as const
@@ -73,7 +70,7 @@ export namespace KilocodeConfig {
worktree?: string
}) {
const dirs = yield* input.fs
.up({ targets: [...ALL_CONFIG_DIR_SUFFIXES], start: input.directory, stop: input.worktree })
.up({ targets: [...KILO_DIR_SUFFIXES], start: input.directory, stop: input.worktree })
.pipe(Effect.orDie)
const roots = yield* input.fs
.up({ targets: [...ALL_CONFIG_FILES], start: input.directory, stop: input.worktree })
@@ -437,6 +434,6 @@ export namespace KilocodeConfig {
/** Check whether a directory path should be treated as a config directory (for loading config files). */
export function isConfigDir(dir: string, flagDir?: string): boolean {
return dir.endsWith(".kilo") || dir.endsWith(".kilocode") || dir.endsWith(".opencode") || dir === flagDir
return dir.endsWith(".kilo") || dir.endsWith(".kilocode") || dir === flagDir
}
}
@@ -71,7 +71,7 @@ export namespace KilocodeConfigOverlay {
}
const files = ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json"] as const
const dirs = [".opencode", ".kilocode", ".kilo"] as const
const dirs = [".kilocode", ".kilo"] as const
const fieldPaths = [
["model"],
@@ -182,12 +182,7 @@ export namespace KilocodeConfigOverlay {
}
function globalDirs() {
return [
Global.Path.config,
path.join(Global.Path.home, ".opencode"),
path.join(Global.Path.home, ".kilocode"),
path.join(Global.Path.home, ".kilo"),
]
return [Global.Path.config, path.join(Global.Path.home, ".kilocode"), path.join(Global.Path.home, ".kilo")]
}
async function withAgents(input: Config.Info, dirs: string[]): Promise<Config.Info> {
@@ -56,7 +56,7 @@ export namespace KilocodeConfigSources {
type Pending = Omit<Source, "order">
const roots = [".opencode", ".kilocode", ".kilo"] as const
const roots = [".kilocode", ".kilo"] as const
const global = ["config.json", "kilo.json", "kilo.jsonc", "opencode.json", "opencode.jsonc"] as const
export async function list(input: Input): Promise<Result> {
@@ -192,7 +192,7 @@ Kilocode skills are automatically discovered and made available in Opencode. Thi
## How It Works
Opencode scans additional directories for skills alongside its native `.opencode/skill/` locations. The `KilocodePaths.skillDirectories()` function provides these paths.
Kilo scans canonical `.kilo/skill/` and `.kilo/skills/` directories alongside legacy `.kilocode/` equivalents. The `KilocodePaths.skillDirectories()` function provides these paths.
## Source Locations
@@ -249,16 +249,7 @@ Detailed instructions for the agent...
## Priority / Override Behavior
When the same skill name exists in multiple locations, **last one wins**:
1. `.claude/skills/` (lowest priority)
2. `.kilocode/skills/` (walk-up)
3. `~/.kilocode/skills/`
4. VSCode extension storage
5. `.opencode/skill/` (walk-up)
6. `~/.opencode/skill/` (highest priority)
This means Opencode native skills take precedence over Kilocode skills with the same name.
When the same skill name exists in multiple locations, **last one wins**. Canonical `.kilo/` skills take precedence over legacy `.kilocode/` skills at the same project level. Explicit external skill paths and compatibility directories are loaded according to the configured scan order.
## Mode-Specific Skills
@@ -286,8 +277,8 @@ Skills can be symlinked from a shared location:
```
.agents/skills/shared-skill/ # Actual skill
.kilocode/skills/shared-skill -> ... # Symlink
.opencode/skill/shared-skill -> ... # Symlink
.kilocode/skills/shared-skill -> ... # Legacy symlink
.kilo/skill/shared-skill -> ... # Canonical symlink
```
The scanner follows symlinks, so a skill installed once can be available to both Kilo VSCode and Opencode CLI.
@@ -70,7 +70,7 @@ The migrator reads rules from these locations:
- `AGENTS.md` in project root
- `CLAUDE.md` in project root
- `~/.opencode/AGENTS.md` (global)
- `~/.config/kilo/AGENTS.md` (global)
## Not Migrated
@@ -5,9 +5,9 @@ import { KilocodePaths } from "@/kilocode/paths"
export namespace ConfigProtection {
/**
* Config directory prefixes (relative paths, forward-slash normalized).
* Matches .kilo/, .kilocode/, .opencode/ at any depth within the project.
* Matches .kilo/ and legacy .kilocode/ at any depth within the project.
*/
const CONFIG_DIRS = [".kilo/", ".kilocode/", ".opencode/"]
const CONFIG_DIRS = [".kilo/", ".kilocode/"]
/**
* Subdirectories under CONFIG_DIRS that are NOT config files (e.g. plan files).
@@ -2,11 +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/`, plus Agent Manager setup/run scripts in the VS Code extension.
This also covers where Kilo looks for config files, commands, agents, and skills across project, global, and legacy paths such as `.kilo/`, `.kilocode/`, and `~/.config/kilo/`, plus Agent Manager setup/run scripts in the VS Code extension.
## Commands (`.kilo/command/*.md`)
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.
Markdown files with YAML frontmatter. The filename (minus `.md`) becomes the command name invoked via `/name`. Commands can live in `.kilo/`, legacy `.kilocode/`, and global config roots, with both `command/` and `commands/` directory names supported. See Config File Locations for the full search order.
```yaml
---
@@ -29,9 +29,8 @@ When asked where `/name` lives, do not search only the repo root. Search these r
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
4. The `KILO_CONFIG_DIR` directory (if the env var is set)
5. project `.kilo/` and `.kilocode/` directories from the current working directory up to the worktree root
Use exact patterns first:
@@ -42,7 +41,7 @@ If found, return the full path. If not found in those roots, explain that the co
## Agents (`.kilo/agent/*.md`)
Also loaded from `.kilocode/` and `.opencode/` directories (legacy), and plural `agents/` variants.
Also loaded from legacy `.kilocode/` directories and plural `agents/` variants.
```yaml
---
@@ -257,7 +256,7 @@ Additional skill directories and remote URLs:
}
```
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.
Skills are markdown files at `skills/<name>/SKILL.md` (or `skill/<name>/SKILL.md`) with `name` and `description` in frontmatter. Discovered inside `.kilo/` and legacy `.kilocode/` directories.
## Other Top-Level Fields
@@ -340,14 +339,14 @@ Notification settings are managed through `kilo console` under **Settings > CLI
| 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`.
Each config directory (`.kilo/` and legacy `.kilocode/`) 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:
Two directory names are scanned: `.kilo` (canonical) and `.kilocode` (legacy fallback). Both are checked at each level, and `.kilo` wins when both define the same entry. `.opencode` directories are not loaded.
- **Project**: walks up from CWD to the git worktree root, checking for all three at each directory level
- **Home**: `~/.kilo/`, `~/.kilocode/`, `~/.opencode/`
- **Project**: walks up from CWD to the git worktree root, checking both directories at each level
- **Home**: `~/.kilo/` and `~/.kilocode/`
- **XDG global**: `~/.config/kilo/` (always loaded, lowest file-based precedence)
### Commands, agents, modes, plugins
@@ -361,7 +360,7 @@ Glob patterns run inside every discovered config directory (including legacy):
| 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.
Example: `~/.config/kilo/command/*.md` (global), `~/.kilocode/command/*.md` (legacy home), and `.kilo/commands/*.md` (project) all load commands.
### Skills and instructions
+1 -1
View File
@@ -23,7 +23,7 @@ export namespace KilocodeTuiConfig {
export type Editable = Omit<Patch, "keybinds"> & { keybinds?: Record<string, string> }
const files = ["tui.jsonc", "tui.json"] as const
const dirs = [".kilo", ".kilocode", ".opencode"] as const
const dirs = [".kilo", ".kilocode"] as const
export async function get(input: { directory: string }) {
const cfg = await Effect.runPromise(
+1 -1
View File
@@ -334,7 +334,7 @@ function patchDir(input: PatchInput) {
if (input.global) return input.config ?? Global.Path.config
const git = input.vcs === "git" && input.worktree !== "/"
const root = git ? input.worktree : input.directory
return path.join(root, ".opencode")
return path.join(root, ".kilo") // kilocode_change
}
function patchName(kind: Kind): "opencode" | "tui" {
+1 -1
View File
@@ -594,7 +594,7 @@ it.instance(
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const skillDir = path.join(test.directory, ".opencode", "skill", "perm-skill")
const skillDir = path.join(test.directory, ".kilo", "skill", "perm-skill") // kilocode_change
yield* Effect.promise(() =>
Bun.write(
path.join(skillDir, "SKILL.md"),
@@ -168,9 +168,9 @@ async function load(): Promise<Data> {
const invalidThemePath = path.join(dir, invalidThemeFile)
const globalThemePath = path.join(dir, globalThemeFile)
const preloadedThemePath = path.join(dir, preloadedThemeFile)
const localDest = path.join(dir, ".opencode", "themes", localThemeFile)
const localDest = path.join(dir, ".kilo", "themes", localThemeFile) // kilocode_change
const globalDest = path.join(Global.Path.config, "themes", globalThemeFile)
const preloadedDest = path.join(dir, ".opencode", "themes", preloadedThemeFile)
const preloadedDest = path.join(dir, ".kilo", "themes", preloadedThemeFile) // kilocode_change
const fnMarker = path.join(dir, "function-called.txt")
const localMarker = path.join(dir, "local-called.json")
const invalidMarker = path.join(dir, "invalid-called.json")
@@ -549,7 +549,7 @@ export default {
.then(() => true)
.catch(() => false)
const leaked_global_to_local = await fs
.stat(path.join(tmp.path, ".opencode", "themes", tmp.extra.globalThemeFile))
.stat(path.join(tmp.path, ".kilo", "themes", tmp.extra.globalThemeFile)) // kilocode_change
.then(() => true)
.catch(() => false)
@@ -1231,7 +1231,7 @@ test("updates installed theme when plugin metadata changes", async () => {
const spec = pathToFileURL(pluginPath).href
const themeFile = "theme-update.json"
const themePath = path.join(dir, themeFile)
const dest = path.join(dir, ".opencode", "themes", themeFile)
const dest = path.join(dir, ".kilo", "themes", themeFile) // kilocode_change
const themeName = themeFile.replace(/\.json$/, "")
const configPath = path.join(dir, "tui.json")
+1 -1
View File
@@ -817,7 +817,7 @@ it.instance("agent markdown permission config preserves user key order", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(
path.join(test.directory, ".opencode", "agent", "ordered.md"),
path.join(test.directory, ".kilo", "agent", "ordered.md"), // kilocode_change
`---
permission:
bash: allow
+4 -4
View File
@@ -87,7 +87,7 @@ it.instance("keeps server and tui plugin merge semantics aligned", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const test = yield* TestInstance
const local = path.join(test.directory, ".opencode")
const local = path.join(test.directory, ".kilo") // kilocode_change
yield* fs.makeDirectory(local, { recursive: true })
yield* fs.writeJson(path.join(Global.Path.config, "kilo.json"), {
@@ -129,7 +129,7 @@ it.instance("loads tui config with the same precedence order as server config pa
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), { theme: "global" })
yield* fs.writeJson(path.join(test.directory, "tui.json"), { theme: "project" })
yield* fs.writeWithDirs(
path.join(test.directory, ".opencode", "tui.json"),
path.join(test.directory, ".kilo", "tui.json"), // kilocode_change
JSON.stringify({ theme: "local", diff_style: "stacked" }, null, 2),
)
@@ -854,7 +854,7 @@ it.instance("silently skips malformed tui.json - load failures degrade to {}", (
const fs = yield* AppFileSystem.Service
const test = yield* TestInstance
yield* fs.writeFileString(path.join(test.directory, "tui.json"), '{ "theme": "broken",')
yield* fs.writeWithDirs(path.join(test.directory, ".opencode", "tui.json"), JSON.stringify({ theme: "fallback" }))
yield* fs.writeWithDirs(path.join(test.directory, ".kilo", "tui.json"), JSON.stringify({ theme: "fallback" })) // kilocode_change
const config = yield* getTuiConfig(test.directory)
expect(config.theme).toBe("fallback")
@@ -868,7 +868,7 @@ it.instance("silently skips non-ENOENT read failures (e.g. tui.json is a directo
const fs = yield* AppFileSystem.Service
const test = yield* TestInstance
yield* fs.makeDirectory(path.join(test.directory, "tui.json"), { recursive: true })
yield* fs.writeWithDirs(path.join(test.directory, ".opencode", "tui.json"), JSON.stringify({ theme: "fallback" }))
yield* fs.writeWithDirs(path.join(test.directory, ".kilo", "tui.json"), JSON.stringify({ theme: "fallback" })) // kilocode_change
const config = yield* getTuiConfig(test.directory)
expect(config.theme).toBe("fallback")
@@ -414,7 +414,7 @@ describe("agent config", () => {
})
describe("project config directory precedence", () => {
test("prefers .kilo, then .kilocode, then .opencode", async () => {
test("prefers .kilo over legacy .kilocode and ignores .opencode", async () => {
await using tmp = await tmpdir()
const entries = [
{
@@ -462,6 +462,7 @@ describe("project config directory precedence", () => {
`---\ndescription: ${item.source} agent\nmode: subagent\n---\n${item.source} agent prompt`,
)
}
await Filesystem.write(path.join(dir, "plugin", `${item.source}.ts`), "export default {}")
}
await provideTestInstance({
@@ -471,7 +472,7 @@ describe("project config directory precedence", () => {
expect(config.username).toBe("kilo")
expect(config.model).toBe("test/kilocode")
expect(config.small_model).toBe("test/opencode")
expect(config.small_model).toBeUndefined()
expect(config.command?.shared).toMatchObject({
description: "kilo command",
@@ -481,10 +482,7 @@ describe("project config directory precedence", () => {
description: "kilocode command",
template: "kilocode command template",
})
expect(config.command?.["opencode-only"]).toMatchObject({
description: "opencode command",
template: "opencode command template",
})
expect(config.command?.["opencode-only"]).toBeUndefined()
expect(config.agent?.shared).toMatchObject({
description: "kilo agent",
@@ -494,10 +492,12 @@ describe("project config directory precedence", () => {
description: "kilocode agent",
prompt: "kilocode agent prompt",
})
expect(config.agent?.["opencode-only"]).toMatchObject({
description: "opencode agent",
prompt: "opencode agent prompt",
})
expect(config.agent?.["opencode-only"]).toBeUndefined()
const plugins = JSON.stringify(config.plugin)
expect(plugins).toContain("kilocode.ts")
expect(plugins).toContain("kilo.ts")
expect(plugins).not.toContain("opencode.ts")
},
})
})
@@ -540,7 +540,7 @@ describe("linked worktree config", () => {
await Bun.write(path.join(directory, "placeholder"), "")
await Bun.write(
path.join(primary.path, "packages", ".opencode", "kilo.jsonc"),
JSON.stringify({ snapshot: true, autoupdate: false, share: "auto" }),
JSON.stringify({ snapshot: true, autoupdate: false, share: "auto", default_agent: "opencode-only" }),
)
await Bun.write(
path.join(primary.path, "packages", ".kilocode", "kilo.jsonc"),
@@ -554,6 +554,7 @@ describe("linked worktree config", () => {
expect(config.snapshot).toBe(false)
expect(config.autoupdate).toBe("notify")
expect(config.share).toBe("manual")
expect(config.default_agent).toBeUndefined()
} finally {
await $`git worktree remove --force ${worktree}`.cwd(primary.path).quiet().nothrow()
}
@@ -11,5 +11,5 @@ async function mark(dir: string) {
}
export function preparePluginDependencies(dir: string) {
return Promise.all([mark(path.join(dir, ".opencode")), mark(Global.Path.config)])
return Promise.all([mark(path.join(dir, ".kilo")), mark(Global.Path.config)])
}
@@ -92,7 +92,7 @@ describe("config overlay routes", () => {
expect(Object.hasOwn(patched, "prototype")).toBe(false)
})
test("prefers .kilo, then .kilocode, then .opencode in project overlays", async () => {
test("prefers .kilo over legacy .kilocode and ignores .opencode in project overlays", async () => {
await using project = await tmpdir()
const entries = [
{
@@ -120,6 +120,10 @@ describe("config overlay routes", () => {
`---\ndescription: ${item.source} agent\nmode: subagent\n---\n${item.source} agent prompt`,
)
}
await Filesystem.write(
path.join(project.path, ".opencode", "agent", "opencode-only.md"),
"---\ndescription: opencode-only agent\nmode: subagent\n---\nopencode-only agent prompt",
)
const body = await KilocodeConfigOverlay.resolve({
directory: project.path,
@@ -131,11 +135,12 @@ describe("config overlay routes", () => {
expect(body.project.username).toBe("kilo")
expect(body.project.model).toBe("test/kilocode")
expect(body.project.small_model).toBe("test/opencode")
expect(body.project.small_model).toBeUndefined()
expect(body.project.agent?.shared).toMatchObject({
description: "kilo agent",
prompt: "kilo agent prompt",
})
expect(body.project.agent?.["opencode-only"]).toBeUndefined()
expect(body.targets.project).toBe(path.join(project.path, ".kilo", "kilo.json"))
})
@@ -112,9 +112,9 @@ describe("config source routes", () => {
const inline = body.sources.find((source) => source.source === "KILO_CONFIG_CONTENT")
expect(order(body, envFile)).toBeLessThan(order(body, projectFile))
expect(order(body, projectFile)).toBeLessThan(order(body, opencodeFile))
expect(order(body, opencodeFile)).toBeLessThan(order(body, kilocodeFile))
expect(order(body, projectFile)).toBeLessThan(order(body, kilocodeFile))
expect(order(body, kilocodeFile)).toBeLessThan(order(body, configFile))
expect(body.sources.some((source) => source.path === opencodeFile)).toBe(false)
expect(order(body, configFile)).toBeLessThan(order(body, extraFile))
expect(inline?.order).toBeGreaterThan(order(body, extraFile))
expect(inline?.order).toBeLessThan(order(body, managedFile))
@@ -245,10 +245,10 @@ export const kiloScenarios: Scenario[] = [
Effect.gen(function* () {
const location = yield* file(
ctx,
".opencode/skill/httpapi-remove/SKILL.md",
".kilo/skill/httpapi-remove/SKILL.md",
"---\nname: httpapi-remove\ndescription: HTTP API removal fixture.\n---\n# HTTP API remove\n",
)
const sentinel = yield* file(ctx, ".opencode/skill/httpapi-remove/KEEP.txt", "synthetic sentinel\n")
const sentinel = yield* file(ctx, ".kilo/skill/httpapi-remove/KEEP.txt", "synthetic sentinel\n")
return { location, sentinel }
}),
)
@@ -273,9 +273,7 @@ export const kiloScenarios: Scenario[] = [
http.protected
.post("/kilocode/agent/remove", "kilocode.removeAgent")
.mutating()
.seeded((ctx) =>
file(ctx, ".opencode/agent/httpapi-remove.md", "---\ndescription: HTTP API remove\n---\nRemove me.\n"),
)
.seeded((ctx) => file(ctx, ".kilo/agent/httpapi-remove.md", "---\ndescription: HTTP API remove\n---\nRemove me.\n"))
.at((ctx) => ({ path: "/kilocode/agent/remove", headers: ctx.headers(), body: { name: "httpapi-remove" } }))
.jsonEffect(200, (body, ctx) =>
Effect.gen(function* () {
@@ -43,6 +43,25 @@ describe("TUI config routes", () => {
expect(body.plugin_origins).toBeUndefined()
})
test("loads legacy .kilocode TUI config and ignores .opencode", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
await fs.mkdir(path.join(dir, ".kilocode"), { recursive: true })
await Bun.write(path.join(dir, ".opencode", "tui.json"), JSON.stringify({ theme: "dracula" }))
await Bun.write(path.join(dir, ".kilocode", "tui.json"), JSON.stringify({ theme: "nord" }))
},
})
const response = await Server.Default().app.request("/tui/config", {
headers: { "x-kilo-directory": tmp.path },
})
expect(response.status).toBe(200)
const body = (await response.json()) as { theme?: string }
expect(body.theme).toBe("nord")
})
test("lists valid TUI keybinds", async () => {
await using tmp = await tmpdir()
@@ -51,7 +51,7 @@ describe("plugin.auth-override", () => {
Effect.gen(function* () {
const tmp = yield* TestInstance
const fs = yield* AppFileSystem.Service
const pluginDir = path.join(tmp.directory, ".opencode", "plugin")
const pluginDir = path.join(tmp.directory, ".kilo", "plugin") // kilocode_change
yield* fs.writeWithDirs(
path.join(pluginDir, "custom-copilot-auth.ts"),
@@ -82,7 +82,7 @@ describe("plugin.install.concurrent", () => {
expect(out.map((x) => x.code)).toEqual(Array.from({ length: all.length }, () => 0))
expect(out.map((x) => x.stderr.toString()).filter(Boolean)).toEqual([])
const cfg = await read(path.join(tmp.path, ".opencode", "opencode.jsonc"))
const cfg = await read(path.join(tmp.path, ".kilo", "opencode.jsonc")) // kilocode_change
expectPlugins(cfg.plugin, all)
}, 25_000)
@@ -105,8 +105,10 @@ describe("plugin.install.concurrent", () => {
expect(out.map((x) => x.code)).toEqual(Array.from({ length: all.length }, () => 0))
expect(out.map((x) => x.stderr.toString()).filter(Boolean)).toEqual([])
const server = await read(path.join(tmp.path, ".opencode", "opencode.jsonc"))
const tui = await read(path.join(tmp.path, ".opencode", "tui.jsonc"))
// kilocode_change start
const server = await read(path.join(tmp.path, ".kilo", "opencode.jsonc"))
const tui = await read(path.join(tmp.path, ".kilo", "tui.jsonc"))
// kilocode_change end
expectPlugins(server.plugin, all)
expectPlugins(tui.plugin, all)
}, 25_000)
@@ -114,7 +116,7 @@ describe("plugin.install.concurrent", () => {
test("preserves updates when existing config uses .json", async () => {
await using tmp = await tmpdir()
const target = await plugin(tmp.path, ["server"])
const cfg = path.join(tmp.path, ".opencode", "opencode.json")
const cfg = path.join(tmp.path, ".kilo", "opencode.json") // kilocode_change
await fs.mkdir(path.dirname(cfg), { recursive: true })
await Bun.write(cfg, JSON.stringify({ plugin: ["seed@1.0.0"] }, null, 2))
@@ -135,6 +137,6 @@ describe("plugin.install.concurrent", () => {
const json = await read(cfg)
expectPlugins(json.plugin, ["seed@1.0.0", ...next])
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) // kilocode_change
}, 25_000)
})
+46 -30
View File
@@ -122,8 +122,10 @@ describe("plugin.install.task", () => {
const ok = await run(ctx(tmp.path))
expect(ok).toBe(true)
const server = await read(path.join(tmp.path, ".opencode", "opencode.jsonc"))
const tui = await read(path.join(tmp.path, ".opencode", "tui.jsonc"))
// kilocode_change start
const server = await read(path.join(tmp.path, ".kilo", "opencode.jsonc"))
const tui = await read(path.join(tmp.path, ".kilo", "tui.jsonc"))
// kilocode_change end
expect(server.plugin).toEqual(["acme@1.2.3"])
expect(tui.plugin).toEqual(["acme@1.2.3"])
})
@@ -144,8 +146,10 @@ describe("plugin.install.task", () => {
const ok = await run(ctx(tmp.path))
expect(ok).toBe(true)
const server = await read(path.join(tmp.path, ".opencode", "opencode.jsonc"))
const tui = await read(path.join(tmp.path, ".opencode", "tui.jsonc"))
// kilocode_change start
const server = await read(path.join(tmp.path, ".kilo", "opencode.jsonc"))
const tui = await read(path.join(tmp.path, ".kilo", "tui.jsonc"))
// kilocode_change end
expect(server.plugin).toEqual([["acme@1.2.3", { custom: true, other: false }]])
expect(tui.plugin).toEqual([["acme@1.2.3", { compact: true }]])
})
@@ -153,7 +157,7 @@ describe("plugin.install.task", () => {
test("preserves JSONC comments when adding plugins to server and tui config", async () => {
await using tmp = await tmpdir()
const target = await plugin(tmp.path, ["server", "tui"])
const cfg = path.join(tmp.path, ".opencode")
const cfg = path.join(tmp.path, ".kilo") // kilocode_change
const server = path.join(cfg, "opencode.jsonc")
const tui = path.join(cfg, "tui.jsonc")
await fs.mkdir(cfg, { recursive: true })
@@ -212,7 +216,7 @@ describe("plugin.install.task", () => {
test("preserves JSONC comments when force replacing plugin version", async () => {
await using tmp = await tmpdir()
const target = await plugin(tmp.path, ["server"])
const cfg = path.join(tmp.path, ".opencode", "opencode.jsonc")
const cfg = path.join(tmp.path, ".kilo", "opencode.jsonc") // kilocode_change
await fs.mkdir(path.dirname(cfg), { recursive: true })
await Bun.write(
cfg,
@@ -257,14 +261,14 @@ describe("plugin.install.task", () => {
const ok = await run(ctx(tmp.path))
expect(ok).toBe(true)
const server = await read(path.join(tmp.path, ".opencode", "opencode.jsonc"))
const server = await read(path.join(tmp.path, ".kilo", "opencode.jsonc")) // kilocode_change
expect(server.plugin).toEqual(["acme@1.2.3"])
})
test("does not change configured package version without force", async () => {
await using tmp = await tmpdir()
const target = await plugin(tmp.path, ["server"])
const cfg = path.join(tmp.path, ".opencode", "opencode.json")
const cfg = path.join(tmp.path, ".kilo", "opencode.json") // kilocode_change
await fs.mkdir(path.dirname(cfg), { recursive: true })
await Bun.write(cfg, JSON.stringify({ plugin: ["acme@1.0.0"] }, null, 2))
@@ -284,7 +288,7 @@ describe("plugin.install.task", () => {
test("does not change scoped package version without force", async () => {
await using tmp = await tmpdir()
const target = await plugin(tmp.path, ["server"])
const cfg = path.join(tmp.path, ".opencode", "opencode.json")
const cfg = path.join(tmp.path, ".kilo", "opencode.json") // kilocode_change
await fs.mkdir(path.dirname(cfg), { recursive: true })
await Bun.write(cfg, JSON.stringify({ plugin: ["@scope/acme@1.0.0"] }, null, 2))
@@ -304,7 +308,7 @@ describe("plugin.install.task", () => {
test("keeps file plugin entries and still adds npm plugin", async () => {
await using tmp = await tmpdir()
const target = await plugin(tmp.path, ["server"])
const cfg = path.join(tmp.path, ".opencode", "opencode.json")
const cfg = path.join(tmp.path, ".kilo", "opencode.json") // kilocode_change
await fs.mkdir(path.dirname(cfg), { recursive: true })
await Bun.write(cfg, JSON.stringify({ plugin: ["file:///tmp/acme.ts"] }, null, 2))
@@ -324,7 +328,7 @@ describe("plugin.install.task", () => {
test("force replaces configured package version and keeps tuple options", async () => {
await using tmp = await tmpdir()
const target = await plugin(tmp.path, ["server"])
const cfg = path.join(tmp.path, ".opencode", "opencode.json")
const cfg = path.join(tmp.path, ".kilo", "opencode.json") // kilocode_change
await fs.mkdir(path.dirname(cfg), { recursive: true })
await Bun.write(
cfg,
@@ -367,7 +371,7 @@ describe("plugin.install.task", () => {
expect(ok).toBe(true)
expect(await Filesystem.exists(path.join(global, "opencode.jsonc"))).toBe(true)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) // kilocode_change
})
test("writes local scope under directory when vcs is not git", async () => {
@@ -386,8 +390,10 @@ describe("plugin.install.task", () => {
const ok = await run(ctxDir(directory, worktree))
expect(ok).toBe(true)
expect(await Filesystem.exists(path.join(directory, ".opencode", "opencode.jsonc"))).toBe(true)
expect(await Filesystem.exists(path.join(worktree, ".opencode", "opencode.jsonc"))).toBe(false)
// kilocode_change start
expect(await Filesystem.exists(path.join(directory, ".kilo", "opencode.jsonc"))).toBe(true)
expect(await Filesystem.exists(path.join(worktree, ".kilo", "opencode.jsonc"))).toBe(false)
// kilocode_change end
})
test("writes local scope under directory when worktree is root slash", async () => {
@@ -404,7 +410,7 @@ describe("plugin.install.task", () => {
const ok = await run(ctxRoot(directory))
expect(ok).toBe(true)
expect(await Filesystem.exists(path.join(directory, ".opencode", "opencode.jsonc"))).toBe(true)
expect(await Filesystem.exists(path.join(directory, ".kilo", "opencode.jsonc"))).toBe(true) // kilocode_change
})
test("writes tui local scope under directory when worktree is root slash", async () => {
@@ -421,7 +427,7 @@ describe("plugin.install.task", () => {
const ok = await run(ctxRoot(directory))
expect(ok).toBe(true)
expect(await Filesystem.exists(path.join(directory, ".opencode", "tui.jsonc"))).toBe(true)
expect(await Filesystem.exists(path.join(directory, ".kilo", "tui.jsonc"))).toBe(true) // kilocode_change
})
test("writes only tui config for tui-only plugins", async () => {
@@ -436,8 +442,10 @@ describe("plugin.install.task", () => {
const ok = await run(ctx(tmp.path))
expect(ok).toBe(true)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "tui.jsonc"))).toBe(true)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false)
// kilocode_change start
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "tui.jsonc"))).toBe(true)
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false)
// kilocode_change end
})
test("writes tui config for oc-themes-only packages", async () => {
@@ -454,10 +462,12 @@ describe("plugin.install.task", () => {
const ok = await run(ctx(tmp.path))
expect(ok).toBe(true)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "tui.jsonc"))).toBe(true)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false)
// kilocode_change start
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "tui.jsonc"))).toBe(true)
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false)
// kilocode_change end
const tui = await read(path.join(tmp.path, ".opencode", "tui.jsonc"))
const tui = await read(path.join(tmp.path, ".kilo", "tui.jsonc")) // kilocode_change
expect(tui.plugin).toEqual(["acme@1.2.3"])
})
@@ -473,15 +483,19 @@ describe("plugin.install.task", () => {
const ok = await run(ctx(tmp.path))
expect(ok).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "tui.jsonc"))).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false)
// kilocode_change start
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "tui.jsonc"))).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false)
// kilocode_change end
})
test("force replaces version in both server and tui configs", async () => {
await using tmp = await tmpdir()
const target = await plugin(tmp.path, ["server", "tui"])
const server = path.join(tmp.path, ".opencode", "opencode.json")
const tui = path.join(tmp.path, ".opencode", "tui.json")
// kilocode_change start
const server = path.join(tmp.path, ".kilo", "opencode.json")
const tui = path.join(tmp.path, ".kilo", "tui.json")
// kilocode_change end
await fs.mkdir(path.dirname(server), { recursive: true })
await Bun.write(server, JSON.stringify({ plugin: ["acme@1.0.0", "other@1.0.0"] }, null, 2))
await Bun.write(tui, JSON.stringify({ plugin: [["acme@1.0.0", { mode: "safe" }], "other@1.0.0"] }, null, 2))
@@ -505,7 +519,7 @@ describe("plugin.install.task", () => {
test("returns false and keeps config unchanged for invalid JSONC", async () => {
await using tmp = await tmpdir()
const target = await plugin(tmp.path, ["server"])
const cfg = path.join(tmp.path, ".opencode", "opencode.jsonc")
const cfg = path.join(tmp.path, ".kilo", "opencode.jsonc") // kilocode_change
await fs.mkdir(path.dirname(cfg), { recursive: true })
const bad = '{"plugin": ["acme@1.0.0",}'
await Bun.write(cfg, bad)
@@ -534,8 +548,10 @@ describe("plugin.install.task", () => {
const ok = await run(ctx(tmp.path))
expect(ok).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "tui.jsonc"))).toBe(false)
// kilocode_change start
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "tui.jsonc"))).toBe(false)
// kilocode_change end
})
test("returns false when manifest cannot be read", async () => {
@@ -551,7 +567,7 @@ describe("plugin.install.task", () => {
const ok = await run(ctx(tmp.path))
expect(ok).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) // kilocode_change
})
test("returns false when install fails", async () => {
@@ -565,6 +581,6 @@ describe("plugin.install.task", () => {
const ok = await run(ctx(tmp.path))
expect(ok).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false)
expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) // kilocode_change
})
})
@@ -1640,7 +1640,7 @@ const provideMultiInstance = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
it.effect("plugin config providers persist after instance dispose", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const configDir = path.join(dir, ".opencode")
const configDir = path.join(dir, ".kilo") // kilocode_change
const root = path.join(configDir, "plugin")
yield* Effect.promise(() => mkdir(root, { recursive: true }))
yield* Effect.promise(() => markPluginDependenciesReady(configDir))
@@ -1697,7 +1697,7 @@ it.instance(
"plugin config enabled and disabled providers are honored",
Effect.gen(function* () {
const instance = yield* TestInstance
const configDir = path.join(instance.directory, ".opencode")
const configDir = path.join(instance.directory, ".kilo") // kilocode_change
const root = path.join(configDir, "plugin")
yield* Effect.promise(() => mkdir(root, { recursive: true }))
yield* Effect.promise(() => markPluginDependenciesReady(configDir))
@@ -122,7 +122,7 @@ function writeProviderAuthPlugin(dir: string) {
yield* Effect.promise(() => preparePluginDependencies(dir)) // kilocode_change
yield* fs.writeWithDirs(
path.join(dir, ".opencode", "plugin", "provider-oauth-parity.ts"),
path.join(dir, ".kilo", "plugin", "provider-oauth-parity.ts"), // kilocode_change
[
"export default {",
' id: "test.provider-oauth-parity",',
@@ -157,7 +157,7 @@ function writeProviderAuthValidationPlugin(dir: string) {
yield* Effect.promise(() => preparePluginDependencies(dir)) // kilocode_change
yield* fs.writeWithDirs(
path.join(dir, ".opencode", "plugin", "provider-oauth-validation.ts"),
path.join(dir, ".kilo", "plugin", "provider-oauth-validation.ts"), // kilocode_change
[
"export default {",
' id: "test.provider-oauth-validation",',
@@ -199,7 +199,7 @@ function writeFunctionOptionsPlugin(dir: string) {
yield* Effect.promise(() => preparePluginDependencies(dir)) // kilocode_change
yield* fs.writeWithDirs(
path.join(dir, ".opencode", "plugin", "provider-function-options.ts"),
path.join(dir, ".kilo", "plugin", "provider-function-options.ts"), // kilocode_change
[
"export default {",
' id: "test.provider-function-options",',
@@ -231,7 +231,7 @@ function writeProviderModelsMutationPlugin(dir: string) {
yield* Effect.promise(() => preparePluginDependencies(dir)) // kilocode_change
yield* fs.writeWithDirs(
path.join(dir, ".opencode", "plugin", "provider-models-mutation.ts"),
path.join(dir, ".kilo", "plugin", "provider-models-mutation.ts"), // kilocode_change
[
"export default {",
' id: "test.provider-models-mutation",',
@@ -294,7 +294,7 @@ function writeStandardFiles(dir: string) {
function writeProjectSkill(dir: string) {
return AppFileSystem.Service.use((fs) =>
fs.writeWithDirs(
path.join(dir, ".opencode", "skills", "project-rest-skill", "SKILL.md"),
path.join(dir, ".kilo", "skills", "project-rest-skill", "SKILL.md"), // kilocode_change
`---
name: project-rest-skill
description: A project skill visible to REST API prompts.
+26 -10
View File
@@ -194,10 +194,12 @@ Just some content without YAML frontmatter.
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
// kilocode_change start - load .kilo skills without falling back to .opencode
yield* Effect.promise(() =>
Bun.write(
path.join(dir, ".opencode", "skill", "manual-skill", "SKILL.md"),
`---
Promise.all([
Bun.write(
path.join(dir, ".kilo", "skill", "manual-skill", "SKILL.md"),
`---
name: manual-skill
---
@@ -205,8 +207,18 @@ name: manual-skill
Instructions here.
`,
),
),
Bun.write(
path.join(dir, ".opencode", "skill", "ignored-skill", "SKILL.md"),
`---
name: ignored-skill
description: This skill must not load.
---
`,
),
]),
)
// kilocode_change end
const skill = yield* Skill.Service
const list = discovered(yield* skill.all()) // kilocode_change
@@ -485,16 +497,18 @@ description: A skill in the .agents/skills directory.
# Agent Skill
`,
),
// kilocode_change start
Bun.write(
path.join(dir, ".opencode", "skill", "opencode-skill", "SKILL.md"),
path.join(dir, ".kilo", "skill", "opencode-skill", "SKILL.md"),
`---
name: opencode-skill
description: A skill in the .opencode/skill directory.
description: A skill in the .kilo/skill directory.
---
# OpenCode Skill
`,
),
// kilocode_change end
]),
)
@@ -532,26 +546,28 @@ description: A skill in the .agents/skills directory.
# Agent Skill
`,
),
// kilocode_change start
Bun.write(
path.join(dir, ".opencode", "skill", "agent-skill", "SKILL.md"),
path.join(dir, ".kilo", "skill", "agent-skill", "SKILL.md"),
`---
name: opencode-skill
description: A skill in the .opencode/skill directory.
description: A skill in the .kilo/skill directory.
---
# OpenCode Skill
`,
),
Bun.write(
path.join(dir, ".opencode", "skills", "agent-skill", "SKILL.md"),
path.join(dir, ".kilo", "skills", "agent-skill", "SKILL.md"),
`---
name: opencode-skill
description: A skill in the .opencode/skills directory.
description: A skill in the .kilo/skills directory.
---
# OpenCode Skill
`,
),
// kilocode_change end
]),
)
+13 -13
View File
@@ -40,7 +40,7 @@ import { run as runSandbox, type Profile } from "@kilocode/sandbox" // kilocode_
const node = CrossSpawnSpawner.defaultLayer
const configLayer = TestConfig.layer({
directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".opencode")])),
directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".kilo")])), // kilocode_change
})
type RegistryLayerOptions = {
@@ -206,10 +206,10 @@ describe("tool.registry", () => {
}),
)
it.instance("loads tools from .opencode/tool (singular)", () =>
it.instance("loads tools from .kilo/tool (singular)" /* kilocode_change */, () =>
Effect.gen(function* () {
const test = yield* TestInstance
const opencode = path.join(test.directory, ".opencode")
const opencode = path.join(test.directory, ".kilo") // kilocode_change
const tool = path.join(opencode, "tool")
yield* Effect.promise(() => fs.mkdir(tool, { recursive: true }))
yield* Effect.promise(() =>
@@ -233,10 +233,10 @@ describe("tool.registry", () => {
}),
)
it.instance("ignores non-tool exports in .opencode/tool files", () =>
it.instance("ignores non-tool exports in .kilo/tool files" /* kilocode_change */, () =>
Effect.gen(function* () {
const test = yield* TestInstance
const tool = path.join(test.directory, ".opencode", "tool")
const tool = path.join(test.directory, ".kilo", "tool") // kilocode_change
yield* Effect.promise(() => fs.mkdir(tool, { recursive: true }))
yield* Effect.promise(() =>
Bun.write(
@@ -269,7 +269,7 @@ describe("tool.registry", () => {
it.instance("tolerates a custom tool exporting null/undefined args (no-args fallback)", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const tool = path.join(test.directory, ".opencode", "tool")
const tool = path.join(test.directory, ".kilo", "tool") // kilocode_change
yield* Effect.promise(() => fs.mkdir(tool, { recursive: true }))
yield* Effect.promise(() =>
Bun.write(
@@ -311,10 +311,10 @@ describe("tool.registry", () => {
}),
)
it.instance("loads tools from .opencode/tools (plural)", () =>
it.instance("loads tools from .kilo/tools (plural)" /* kilocode_change */, () =>
Effect.gen(function* () {
const test = yield* TestInstance
const opencode = path.join(test.directory, ".opencode")
const opencode = path.join(test.directory, ".kilo") // kilocode_change
const tools = path.join(opencode, "tools")
yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
yield* Effect.promise(() =>
@@ -341,7 +341,7 @@ describe("tool.registry", () => {
it.instance("loads Zod-schema custom tools with JSON Schema and validation", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const customTools = path.join(test.directory, ".opencode", "tools")
const customTools = path.join(test.directory, ".kilo", "tools") // kilocode_change
const pluginTool = pathToFileURL(path.resolve(import.meta.dir, "../../../plugin/src/tool.ts")).href
yield* Effect.promise(() => fs.mkdir(customTools, { recursive: true }))
yield* Effect.promise(() =>
@@ -394,7 +394,7 @@ describe("tool.registry", () => {
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const opencode = path.join(test.directory, ".opencode")
const opencode = path.join(test.directory, ".kilo") // kilocode_change
const customTools = path.join(opencode, "tools")
const plugin = path.join(opencode, "node_modules", "@kilocode", "plugin") // kilocode_change
yield* Effect.promise(() => fs.mkdir(path.join(plugin, "dist"), { recursive: true }))
@@ -459,7 +459,7 @@ describe("tool.registry", () => {
it.instance("preserves attachments from structured custom tool results", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const customTools = path.join(test.directory, ".opencode", "tools")
const customTools = path.join(test.directory, ".kilo", "tools") // kilocode_change
const pluginTool = pathToFileURL(path.resolve(import.meta.dir, "../../../plugin/src/tool.ts")).href
yield* Effect.promise(() => fs.mkdir(customTools, { recursive: true }))
yield* Effect.promise(() =>
@@ -504,7 +504,7 @@ describe("tool.registry", () => {
it.instance("loads legacy JSON-schema-shaped custom tools with wire schema", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const tools = path.join(test.directory, ".opencode", "tools")
const tools = path.join(test.directory, ".kilo", "tools") // kilocode_change
yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
yield* Effect.promise(() =>
Bun.write(
@@ -536,7 +536,7 @@ describe("tool.registry", () => {
it.instance("loads tools with external dependencies without crashing", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const opencode = path.join(test.directory, ".opencode")
const opencode = path.join(test.directory, ".kilo") // kilocode_change
const tools = path.join(opencode, "tools")
yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
yield* Effect.promise(() =>
+1 -1
View File
@@ -36,7 +36,7 @@ describe("tool.skill", () => {
unix("execute returns skill content block with files", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const skill = path.join(dir, ".opencode", "skill", "tool-skill")
const skill = path.join(dir, ".kilo", "skill", "tool-skill") // kilocode_change
yield* Effect.promise(() =>
Bun.write(
path.join(skill, "SKILL.md"),