diff --git a/.changeset/fresh-rules-list.md b/.changeset/fresh-rules-list.md new file mode 100644 index 00000000000..0266967dcc1 --- /dev/null +++ b/.changeset/fresh-rules-list.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Add `/rules` in the CLI to list rule files loaded for the current session. diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx b/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx index 92873540006..56d90f74619 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx @@ -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" @@ -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(() => ) + }, + }, { get title() { return isAllowEverything(sync.data.config.permission) ? "Disable auto-approve mode" : "Enable auto-approve mode" diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-rules.tsx b/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-rules.tsx new file mode 100644 index 00000000000..e309e2b6da9 --- /dev/null +++ b/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-rules.tsx @@ -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[]>(() => + (rules() ?? []).map((rule) => ({ + title: rule.name, + description: rule.path, + value: rule.path, + category: "Rules", + })), + ) + + return +} diff --git a/packages/opencode/src/kilocode/rules.ts b/packages/opencode/src/kilocode/rules.ts new file mode 100644 index 00000000000..97533c23ef5 --- /dev/null +++ b/packages/opencode/src/kilocode/rules.ts @@ -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 + + 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)) + }) +} diff --git a/packages/opencode/src/server/routes/instance/kilocode.ts b/packages/opencode/src/server/routes/instance/kilocode.ts index ebfacf8980a..c7b99774e31 100644 --- a/packages/opencode/src/server/routes/instance/kilocode.ts +++ b/packages/opencode/src/server/routes/instance/kilocode.ts @@ -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({ diff --git a/packages/opencode/test/kilocode/rules.test.ts b/packages/opencode/test/kilocode/rules.test.ts new file mode 100644 index 00000000000..b70136661c3 --- /dev/null +++ b/packages/opencode/test/kilocode/rules.test.ts @@ -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 = (effect: Effect.Effect) => + 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 + } + } + }) +}) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index d9d3bac64e4..3fa764bd350 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -75,6 +75,7 @@ import type { KilocodeRemoveAgentResponses, KilocodeRemoveSkillErrors, KilocodeRemoveSkillResponses, + KilocodeRulesResponses, KilocodeSessionImportMessageErrors, KilocodeSessionImportMessageResponses, KilocodeSessionImportPartErrors, @@ -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( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/kilocode/rules", + ...options, + ...params, + }) + } + /** * Remove a skill * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index b9d92659382..ef3c1ff5a6a 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -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 diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 75779e397fa..1f4c10cfe11 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -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",