* fix(#783): resolve Kilo global skills base to ~/.kilo/skills getGlobalSkillsBase('kilo') returned ~/.config/kilo/skills (the XDG config dir), but Kilo Code discovers global skills from ~/.kilo/skills/ (the .kilo dir in HOME), independent of the kilo.jsonc config dir. Add a HOME-relative special case so the resolver matches Kilo's actual discovery path. The config dir (~/.config/kilo) and the installer's command/ path are correct and unchanged. This corrects the path used by doctor/status and agent-skills-block resolution; the installer writes commands (not skills) for Kilo, so no files were being written to the wrong location. Closes #783 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#783): set changeset pr to 806 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/783-kilo-global-skills-base.md
Normal file
7
.changeset/783-kilo-global-skills-base.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 806
|
||||
---
|
||||
**`getGlobalSkillsBase('kilo')` now resolves to `~/.kilo/skills`** — where Kilo Code actually discovers global skills — instead of `~/.config/kilo/skills`. Per [Kilo Code docs](https://kilo.ai/docs/customize/skills), global skills live in the `.kilo` directory within HOME (`~/.kilo/skills/`), independent of the XDG-based config dir at `~/.config/kilo`. The kilo.jsonc config dir (`~/.config/kilo`) and the `command/` path used by the installer are correct and unchanged. Blast radius: this corrects the resolved skills-base path used by doctor/status checks and agent-skills-block resolution (`init.cjs`); the installer writes commands (not skills) for Kilo, so no files were previously being written to the wrong location.
|
||||
|
||||
<!-- docs-exempt: internal path-resolution correction; no user-facing how-to surface changed -->
|
||||
@@ -162,8 +162,17 @@ export function getGlobalConfigDir(runtime: string, explicitDir?: string | null)
|
||||
*/
|
||||
export function getGlobalSkillsBase(runtime: string): string | null {
|
||||
if (runtime === 'cline') return null;
|
||||
if (runtime === 'hermes') {
|
||||
const configDir = getGlobalConfigDir(runtime);
|
||||
return path.join(configDir, 'skills', 'gsd');
|
||||
}
|
||||
// Kilo Code discovers global skills from ~/.kilo/skills/ (HOME-relative),
|
||||
// independent of the XDG-based config dir (~/.config/kilo) used for commands.
|
||||
// See: https://kilo.ai/docs/customize/skills
|
||||
// "Global skills are located in the `.kilo` directory within your Home
|
||||
// directory: ~/.kilo/skills/"
|
||||
if (runtime === 'kilo') return path.join(os.homedir(), '.kilo', 'skills');
|
||||
const configDir = getGlobalConfigDir(runtime);
|
||||
if (runtime === 'hermes') return path.join(configDir, 'skills', 'gsd');
|
||||
return path.join(configDir, 'skills');
|
||||
}
|
||||
|
||||
|
||||
105
tests/bug-783-kilo-global-skills-base.test.cjs
Normal file
105
tests/bug-783-kilo-global-skills-base.test.cjs
Normal file
@@ -0,0 +1,105 @@
|
||||
'use strict';
|
||||
// Regression guard for bug #783.
|
||||
//
|
||||
// getGlobalSkillsBase('kilo') was returning ~/.config/kilo/skills (the XDG
|
||||
// config dir) instead of ~/.kilo/skills — where Kilo Code actually discovers
|
||||
// global skills per its docs:
|
||||
// https://kilo.ai/docs/customize/skills
|
||||
// "Global skills are located in the `.kilo` directory within your Home
|
||||
// directory: ~/.kilo/skills/"
|
||||
//
|
||||
// The fix adds a special case in getGlobalSkillsBase() that resolves kilo's
|
||||
// skills dir from HOME (not from the XDG config dir). The config dir at
|
||||
// ~/.config/kilo is still CORRECT for commands (command/) and must stay
|
||||
// unchanged — this test verifies both roles are separate.
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const {
|
||||
getGlobalConfigDir,
|
||||
getGlobalSkillsBase,
|
||||
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-homes.cjs'));
|
||||
|
||||
// Helper: temporarily override env vars for a test, restoring them afterwards.
|
||||
function withEnv(overrides, fn) {
|
||||
const saved = {};
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
saved[key] = process.env[key];
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
for (const [key] of Object.entries(overrides)) {
|
||||
if (saved[key] === undefined) delete process.env[key];
|
||||
else process.env[key] = saved[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all kilo-relevant env vars so tests are hermetic.
|
||||
const kiloEnvClears = {
|
||||
KILO_CONFIG_DIR: undefined,
|
||||
XDG_CONFIG_HOME: undefined,
|
||||
};
|
||||
|
||||
describe('bug #783: kilo global skills dir is ~/.kilo/skills, not ~/.config/kilo/skills', () => {
|
||||
test('getGlobalSkillsBase("kilo") resolves to ~/.kilo/skills', () => {
|
||||
withEnv(kiloEnvClears, () => {
|
||||
assert.strictEqual(
|
||||
getGlobalSkillsBase('kilo'),
|
||||
path.join(os.homedir(), '.kilo', 'skills'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('getGlobalConfigDir("kilo") still resolves to ~/.config/kilo (config dir unchanged)', () => {
|
||||
withEnv(kiloEnvClears, () => {
|
||||
assert.strictEqual(
|
||||
getGlobalConfigDir('kilo'),
|
||||
path.join(os.homedir(), '.config', 'kilo'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('kilo skills dir and config dir are decoupled (not equal, not nested)', () => {
|
||||
withEnv(kiloEnvClears, () => {
|
||||
const skillsBase = getGlobalSkillsBase('kilo');
|
||||
const configDir = getGlobalConfigDir('kilo');
|
||||
|
||||
assert.notStrictEqual(skillsBase, configDir, 'skills dir must differ from config dir');
|
||||
assert.ok(
|
||||
!skillsBase.startsWith(configDir + path.sep),
|
||||
`skills dir (${skillsBase}) must not be nested under config dir (${configDir})`,
|
||||
);
|
||||
assert.ok(
|
||||
!configDir.startsWith(skillsBase + path.sep),
|
||||
`config dir (${configDir}) must not be nested under skills dir (${skillsBase})`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('getGlobalSkillsBase("kilo") is NOT affected by KILO_CONFIG_DIR override', () => {
|
||||
// Skills always live in ~/.kilo/skills regardless of XDG/config-dir overrides.
|
||||
withEnv({ KILO_CONFIG_DIR: '/tmp/custom-kilo-config', XDG_CONFIG_HOME: undefined }, () => {
|
||||
assert.strictEqual(
|
||||
getGlobalSkillsBase('kilo'),
|
||||
path.join(os.homedir(), '.kilo', 'skills'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('getGlobalSkillsBase("kilo") is NOT affected by XDG_CONFIG_HOME override', () => {
|
||||
withEnv({ KILO_CONFIG_DIR: undefined, XDG_CONFIG_HOME: '/tmp/custom-xdg' }, () => {
|
||||
assert.strictEqual(
|
||||
getGlobalSkillsBase('kilo'),
|
||||
path.join(os.homedir(), '.kilo', 'skills'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user