Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-rules-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Add `/rules` in the CLI to list rule files loaded for the current session.
12 changes: 12 additions & 0 deletions packages/opencode/src/kilocode/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useTheme } from "@tui/context/theme"
import { DialogAlert } from "@tui/ui/dialog-alert"
import { DialogSelect } from "@tui/ui/dialog-select"
import { Link } from "@tui/ui/link"
import { DialogRules } from "./component/dialog-rules"
import { isKiloError, showKiloErrorToast } from "@/kilocode/kilo-errors"
import { registerKiloCommands } from "@/kilocode/kilo-commands"
import { initializeTUIDependencies } from "@kilocode/kilo-gateway/tui"
Expand Down Expand Up @@ -154,6 +155,17 @@ export function init() {

// Register auto-approve toggle
command.register(() => [
{
title: "Rules",
value: "rules.list",
category: "System",
slash: {
name: "rules",
},
onSelect: (dialog) => {
dialog.replace(() => <DialogRules />)
},
},
{
get title() {
return isAllowEverything(sync.data.config.permission) ? "Disable auto-approve mode" : "Enable auto-approve mode"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// kilocode_change - new file
import { createMemo, createResource } from "solid-js"
import { useProject } from "@tui/context/project"
import { useSDK } from "@tui/context/sdk"
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"

export function DialogRules() {
const project = useProject()
const sdk = useSDK()
const dialog = useDialog()
dialog.setSize("large")

const [rules] = createResource(async () => {
const result = await sdk.client.kilocode.rules({ workspace: project.workspace.current() }, { throwOnError: true })
return result.data ?? []
})

const options = createMemo<DialogSelectOption<string>[]>(() =>
(rules() ?? []).map((rule) => ({
title: rule.name,
description: rule.path,
value: rule.path,
category: "Rules",
})),
)

return <DialogSelect title="Rules" placeholder="Search rules..." options={options()} />
}
24 changes: 24 additions & 0 deletions packages/opencode/src/kilocode/rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// kilocode_change - new file
import path from "path"
import { Effect } from "effect"
import z from "zod"
import { Instruction } from "@/session/instruction"

export namespace KiloRules {
export const Info = z.object({
path: z.string(),
name: z.string(),
})
export type Info = z.infer<typeof Info>

export const list = Effect.fn("KiloRules.list")(function* () {
const instruction = yield* Instruction.Service
const paths = yield* instruction.systemPaths()
return Array.from(paths)
.map((file) => ({
path: file,
name: path.basename(file),
}))
.toSorted((a, b) => a.path.localeCompare(b.path))
})
}
24 changes: 24 additions & 0 deletions packages/opencode/src/server/routes/instance/kilocode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,38 @@ import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import { Skill } from "@/skill"
import { Agent } from "@/agent/agent"
import { KiloRules } from "@/kilocode/rules"
import { lazy } from "@/util/lazy"
import { errors } from "../../error"
import { jsonRequest } from "./trace"
import { SessionImportRoutes } from "@/kilocode/session-import/routes"
import { HeapSnapshot } from "@/kilocode/cli/heap-snapshot"

export const KilocodeRoutes = lazy(() =>
new Hono()
.route("/session-import", SessionImportRoutes())
.get(
"/rules",
describeRoute({
summary: "List loaded rules",
description: "List local rule and instruction files currently loaded for this session.",
operationId: "kilocode.rules",
responses: {
200: {
description: "Loaded rules",
content: {
"application/json": {
schema: resolver(KiloRules.Info.array()),
},
},
},
},
}),
async (c) =>
jsonRequest("KilocodeRoutes.rules", c, function* () {
return yield* KiloRules.list()
}),
)
.post(
"/heap/snapshot",
describeRoute({
Expand Down
74 changes: 74 additions & 0 deletions packages/opencode/test/kilocode/rules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { afterEach, describe, expect, test } from "bun:test"
import * as fs from "fs/promises"
import path from "path"
import { Effect } from "effect"
import { Global } from "../../src/global"
import { KiloRules } from "../../src/kilocode/rules"
import { Instance } from "../../src/project/instance"
import { Instruction } from "../../src/session/instruction"
import { tmpdir } from "../fixture/fixture"

const run = <A>(effect: Effect.Effect<A, any, Instruction.Service>) =>
Effect.runPromise(effect.pipe(Effect.provide(Instruction.defaultLayer)))

afterEach(async () => {
await Instance.disposeAll()
})

describe("KiloRules.list", () => {
test("lists loaded project, global, and configured rule files", async () => {
const originalConfigDir = process.env["KILO_CONFIG_DIR"]
delete process.env["KILO_CONFIG_DIR"]

await using globalTmp = await tmpdir({
init: async (dir) => {
await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions")
},
})
await using projectTmp = await tmpdir({
config: { instructions: [".kilo/rules/style.md"] },
init: async (dir) => {
await Bun.write(path.join(dir, "AGENTS.md"), "# Project Instructions")
await fs.mkdir(path.join(dir, ".kilo", "rules"), { recursive: true })
await Bun.write(path.join(dir, ".kilo", "rules", "style.md"), "# Style Rules")
},
})

const originalGlobalConfig = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp.path

try {
await Instance.provide({
directory: projectTmp.path,
fn: () =>
run(
Effect.gen(function* () {
const rules = yield* KiloRules.list()
const paths = rules.map((rule) => rule.path)

expect(rules).toContainEqual({
path: path.join(projectTmp.path, ".kilo", "rules", "style.md"),
name: "style.md",
})
expect(rules).toContainEqual({
path: path.join(projectTmp.path, "AGENTS.md"),
name: "AGENTS.md",
})
expect(rules).toContainEqual({
path: path.join(globalTmp.path, "AGENTS.md"),
name: "AGENTS.md",
})
expect(paths).toEqual(paths.toSorted())
}),
),
})
} finally {
;(Global.Path as { config: string }).config = originalGlobalConfig
if (originalConfigDir === undefined) {
delete process.env["KILO_CONFIG_DIR"]
} else {
process.env["KILO_CONFIG_DIR"] = originalConfigDir
}
}
})
})
31 changes: 31 additions & 0 deletions packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import type {
KilocodeRemoveAgentResponses,
KilocodeRemoveSkillErrors,
KilocodeRemoveSkillResponses,
KilocodeRulesResponses,
KilocodeSessionImportMessageErrors,
KilocodeSessionImportMessageResponses,
KilocodeSessionImportPartErrors,
Expand Down Expand Up @@ -5506,6 +5507,36 @@ export class Heap extends HeyApiClient {
}

export class Kilocode extends HeyApiClient {
/**
* List loaded rules
*
* List local rule and instruction files currently loaded for this session.
*/
public rules<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<KilocodeRulesResponses, unknown, ThrowOnError>({
url: "/kilocode/rules",
...options,
...params,
})
}

/**
* Remove a skill
*
Expand Down
22 changes: 22 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6839,6 +6839,28 @@ export type KilocodeSessionImportPartResponses = {
export type KilocodeSessionImportPartResponse =
KilocodeSessionImportPartResponses[keyof KilocodeSessionImportPartResponses]

export type KilocodeRulesData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/rules"
}

export type KilocodeRulesResponses = {
/**
* Loaded rules
*/
200: Array<{
path: string
name: string
}>
}

export type KilocodeRulesResponse = KilocodeRulesResponses[keyof KilocodeRulesResponses]

export type KilocodeHeapSnapshotData = {
body?: never
path?: never
Expand Down
53 changes: 53 additions & 0 deletions packages/sdk/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9649,6 +9649,59 @@
]
}
},
"/kilocode/rules": {
"get": {
"operationId": "kilocode.rules",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "workspace",
"schema": {
"type": "string"
}
}
],
"summary": "List loaded rules",
"description": "List local rule and instruction files currently loaded for this session.",
"responses": {
"200": {
"description": "Loaded rules",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": ["path", "name"]
}
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilocode.rules({\n ...\n})"
}
]
}
},
"/kilocode/heap/snapshot": {
"post": {
"operationId": "kilocode.heap.snapshot",
Expand Down
Loading