feat: add DELETE /skill endpoint and use CLI backend for skill removal

Add Skill.remove() that deletes the skill directory from disk and
mutates the cached Instance.state in-place so subsequent GET /skill
calls return the updated list.

Add DELETE /skill?location=... server endpoint, regenerate the SDK,
and update KiloProvider to call the SDK method instead of deleting
files directly from the extension.
This commit is contained in:
Mark IJbema
2026-03-16 13:04:07 +01:00
parent 3da18dee40
commit ec544e8288
6 changed files with 171 additions and 5 deletions
+5 -5
View File
@@ -1133,15 +1133,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
/**
* Remove a skill by deleting its directory from disk, then refresh the skills list.
* The location is the path to the SKILL.md file; the skill directory is its parent.
* Remove a skill via the CLI backend (deletes from disk + clears cache), then refresh.
*/
private async handleRemoveSkill(location: string): Promise<void> {
const dir = path.dirname(location)
if (!this.client) return
try {
await vscode.workspace.fs.delete(vscode.Uri.file(dir), { recursive: true })
const dir = this.getWorkspaceDirectory()
await this.client.app.removeSkill({ location, directory: dir }, { throwOnError: true })
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to delete skill directory:", dir, error)
console.error("[Kilo New] KiloProvider: Failed to remove skill:", error)
}
this.cachedSkillsMessage = null
await this.fetchAndSendSkills()
+32
View File
@@ -500,6 +500,38 @@ export namespace Server {
return c.json(skills)
},
)
// kilocode_change start
.delete(
"/skill",
describeRoute({
summary: "Remove a skill",
description: "Remove a skill by deleting its directory from disk and clearing it from cache.",
operationId: "app.removeSkill",
responses: {
200: {
description: "Skill removed",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"query",
z.object({
location: z.string(),
}),
),
async (c) => {
const { location } = c.req.valid("query")
await Skill.remove(location)
return c.json(true)
},
)
// kilocode_change end
.get(
"/lsp",
describeRoute({
+14
View File
@@ -1,6 +1,7 @@
import z from "zod"
import path from "path"
import os from "os"
import { rm } from "fs/promises"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
import { NamedError } from "@opencode-ai/util/error"
@@ -213,4 +214,17 @@ export namespace Skill {
export async function dirs() {
return state().then((x) => x.dirs)
}
// kilocode_change start
export async function remove(location: string) {
const dir = path.dirname(location)
await rm(dir, { recursive: true, force: true })
const s = await state()
const name = Object.keys(s.skills).find((k) => s.skills[k].location === location)
if (name) {
delete s.skills[name]
s.dirs = s.dirs.filter((d) => d !== dir)
}
}
// kilocode_change end
}
+34
View File
@@ -7,6 +7,8 @@ import type {
AppAgentsResponses,
AppLogErrors,
AppLogResponses,
AppRemoveSkillErrors,
AppRemoveSkillResponses,
AppSkillsResponses,
Auth as Auth3,
AuthRemoveErrors,
@@ -4330,6 +4332,38 @@ export class App extends HeyApiClient {
})
}
/**
* Remove a skill
*
* Remove a skill by deleting its directory from disk and clearing it from cache.
*/
public removeSkill<ThrowOnError extends boolean = false>(
parameters: {
directory?: string
workspace?: string
location: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "query", key: "location" },
],
},
],
)
return (options?.client ?? this.client).delete<AppRemoveSkillResponses, AppRemoveSkillErrors, ThrowOnError>({
url: "/skill",
...options,
...params,
})
}
/**
* List skills
*
+29
View File
@@ -5483,6 +5483,35 @@ export type AppAgentsResponses = {
export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses]
export type AppRemoveSkillData = {
body?: never
path?: never
query: {
directory?: string
workspace?: string
location: string
}
url: "/skill"
}
export type AppRemoveSkillErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type AppRemoveSkillError = AppRemoveSkillErrors[keyof AppRemoveSkillErrors]
export type AppRemoveSkillResponses = {
/**
* Skill removed
*/
200: boolean
}
export type AppRemoveSkillResponse = AppRemoveSkillResponses[keyof AppRemoveSkillResponses]
export type AppSkillsData = {
body?: never
path?: never
+57
View File
@@ -7935,6 +7935,63 @@
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.app.skills({\n ...\n})"
}
]
},
"delete": {
"operationId": "app.removeSkill",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "workspace",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "location",
"schema": {
"type": "string"
},
"required": true
}
],
"summary": "Remove a skill",
"description": "Remove a skill by deleting its directory from disk and clearing it from cache.",
"responses": {
"200": {
"description": "Skill removed",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.app.removeSkill({\n ...\n})"
}
]
}
},
"/lsp": {