Skip to content

Commit eb0ed0d

Browse files
dyoshikawaClaude Codeclaude
authored
Add Kilo Code workflows as commands (#758)
* Add Kilo Code workflows as commands * chore: update devcontainer and docs - Add goose CLI installation - Remove opencommit and bunfig.toml configuration - Remove duplicate Kilo Code documentation section from README 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Code <[email protected]> Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 03156ae commit eb0ed0d

File tree

8 files changed

+319
-8
lines changed

8 files changed

+319
-8
lines changed

.devcontainer/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ RUN mise trust /workspace && mise install
9797
RUN mise exec -- go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest
9898
ENV PATH=$PATH:/home/node/go/bin
9999

100+
# Install goose
101+
RUN curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash
102+
100103
# Install Safe Chain
101104
RUN curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh
102105

@@ -109,13 +112,10 @@ RUN mkdir -p "$PNPM_HOME" && \
109112
@anthropic-ai/claude-code \
110113
opencode-ai \
111114
@openai/codex \
112-
opencommit \
113115
@google/gemini-cli \
114116
difit \
115117
@musistudio/claude-code-router
116118

117-
# Enable smol mode in Bun to reduce memory usage
118-
RUN echo 'smol = true' > /home/node/bunfig.toml
119119

120120
# Claude Code Router config
121121
COPY ccr.config.json /home/node/.claude-code-router/config.json

README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
136136
| Cursor |||| ✅ 🌏 | 🎮 ||
137137
| OpenCode || || ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
138138
| Cline |||| ✅ 🌏 | | |
139-
| Kilo Code | ✅ 🌏 | | | | | |
139+
| Kilo Code | ✅ 🌏 | | | ✅ 🌏 | | |
140140
| Roo Code ||||| 🎮 | ✅ 🌏 |
141141
| Qwen Code ||| | | | |
142142
| Kiro IDE ||| | | | |
@@ -323,10 +323,6 @@ This is Rulesync, a Node.js CLI tool that automatically generates configuration
323323
...
324324
```
325325
326-
### `.kilocode/rules/*.md` (Kilo Code)
327-
328-
Kilo Code loads project rules from `.kilocode/rules/` and global rules from `~/.kilocode/rules/`. The tool also supports mode-specific directories like `.kilocode/rules-{mode}` and legacy single-file fallbacks such as `.kilocoderules-{mode}` (or `.kilocoderules`, `.clinerules`, and `.roorules`), but Rulesync generates the directory-based layout by default.
329-
330326
### `rulesync/commands/*.md`
331327
332328
Example:

src/cli/commands/gitignore.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ describe("gitignoreCommand", () => {
5656
expect(content).toContain("**/.gemini/memories/");
5757
expect(content).toContain("**/.roo/rules/");
5858
expect(content).toContain("**/.kilocode/rules/");
59+
expect(content).toContain("**/.kilocode/workflows/");
5960
expect(content).toContain("**/.roo/skills/");
6061
expect(content).toContain("**/.aiignore");
6162
expect(content).toContain("**/.mcp.json");
@@ -220,6 +221,7 @@ dist/`;
220221
**/.junie/guidelines.md
221222
**/.junie/mcp.json
222223
**/.kilocode/rules/
224+
**/.kilocode/workflows/
223225
**/.kiro/steering/
224226
**/.aiignore
225227
**/.opencode/memories/

src/cli/commands/gitignore.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const RULESYNC_IGNORE_ENTRIES = [
5454
"**/.junie/mcp.json",
5555
// Kilo Code
5656
"**/.kilocode/rules/",
57+
"**/.kilocode/workflows/",
5758
// Kiro
5859
"**/.kiro/steering/",
5960
"**/.aiignore",

src/features/commands/commands-processor.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ClineCommand } from "./cline-command.js";
99
import { CommandsProcessor, CommandsProcessorToolTarget } from "./commands-processor.js";
1010
import { CursorCommand } from "./cursor-command.js";
1111
import { GeminiCliCommand } from "./geminicli-command.js";
12+
import { KiloCommand } from "./kilo-command.js";
1213
import { OpenCodeCommand } from "./opencode-command.js";
1314
import { RooCommand } from "./roo-command.js";
1415
import { RulesyncCommand } from "./rulesync-command.js";
@@ -41,6 +42,11 @@ vi.mock("./geminicli-command.js", () => ({
4142
return { ...config, isDeletable: () => true };
4243
}),
4344
}));
45+
vi.mock("./kilo-command.js", () => ({
46+
KiloCommand: vi.fn().mockImplementation(function (config) {
47+
return { ...config, isDeletable: () => true };
48+
}),
49+
}));
4450
vi.mock("./opencode-command.js", () => ({
4551
OpenCodeCommand: vi.fn().mockImplementation(function (config) {
4652
return { ...config, isDeletable: () => true };
@@ -102,6 +108,19 @@ vi.mocked(GeminiCliCommand).forDeletion = vi.fn().mockImplementation((params) =>
102108
getRelativeFilePath: () => params.relativeFilePath,
103109
}));
104110

111+
// Set up static methods after mocking
112+
vi.mocked(KiloCommand).fromFile = vi.fn();
113+
vi.mocked(KiloCommand).fromRulesyncCommand = vi.fn();
114+
vi.mocked(KiloCommand).isTargetedByRulesyncCommand = vi.fn().mockReturnValue(true);
115+
vi.mocked(KiloCommand).getSettablePaths = vi
116+
.fn()
117+
.mockReturnValue({ relativeDirPath: join(".kilocode", "workflows") });
118+
vi.mocked(KiloCommand).forDeletion = vi.fn().mockImplementation((params) => ({
119+
...params,
120+
isDeletable: () => true,
121+
getRelativeFilePath: () => params.relativeFilePath,
122+
}));
123+
105124
// Set up static methods after mocking
106125
vi.mocked(OpenCodeCommand).fromFile = vi.fn();
107126
vi.mocked(OpenCodeCommand).fromRulesyncCommand = vi.fn();
@@ -797,6 +816,7 @@ describe("CommandsProcessor", () => {
797816
"copilot",
798817
"cursor",
799818
"geminicli",
819+
"kilo",
800820
"opencode",
801821
"roo",
802822
]),
@@ -815,6 +835,7 @@ describe("CommandsProcessor", () => {
815835
"copilot",
816836
"cursor",
817837
"geminicli",
838+
"kilo",
818839
"opencode",
819840
"roo",
820841
]),
@@ -833,6 +854,7 @@ describe("CommandsProcessor", () => {
833854
"cursor",
834855
"geminicli",
835856
"codexcli",
857+
"kilo",
836858
"opencode",
837859
]),
838860
);
@@ -866,6 +888,7 @@ describe("CommandsProcessor", () => {
866888
"claudecode-legacy",
867889
"cline",
868890
"geminicli",
891+
"kilo",
869892
"roo",
870893
];
871894

src/features/commands/commands-processor.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { CodexcliCommand } from "./codexcli-command.js";
1515
import { CopilotCommand } from "./copilot-command.js";
1616
import { CursorCommand } from "./cursor-command.js";
1717
import { GeminiCliCommand } from "./geminicli-command.js";
18+
import { KiloCommand } from "./kilo-command.js";
1819
import { OpenCodeCommand } from "./opencode-command.js";
1920
import { RooCommand } from "./roo-command.js";
2021
import { RulesyncCommand } from "./rulesync-command.js";
@@ -64,6 +65,7 @@ const commandsProcessorToolTargetTuple = [
6465
"copilot",
6566
"cursor",
6667
"geminicli",
68+
"kilo",
6769
"opencode",
6870
"roo",
6971
] as const;
@@ -146,6 +148,13 @@ const toolCommandFactories = new Map<CommandsProcessorToolTarget, ToolCommandFac
146148
meta: { extension: "toml", supportsProject: true, supportsGlobal: true, isSimulated: false },
147149
},
148150
],
151+
[
152+
"kilo",
153+
{
154+
class: KiloCommand,
155+
meta: { extension: "md", supportsProject: true, supportsGlobal: true, isSimulated: false },
156+
},
157+
],
149158
[
150159
"opencode",
151160
{
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { join } from "node:path";
2+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3+
import { RULESYNC_COMMANDS_RELATIVE_DIR_PATH } from "../../constants/rulesync-paths.js";
4+
import { setupTestDirectory } from "../../test-utils/test-directories.js";
5+
import { writeFileContent } from "../../utils/file.js";
6+
import { KiloCommand } from "./kilo-command.js";
7+
import { RulesyncCommand } from "./rulesync-command.js";
8+
9+
describe("KiloCommand", () => {
10+
let testDir: string;
11+
let cleanup: () => Promise<void>;
12+
13+
const validContent = "# Sample workflow\n\nFollow these steps.";
14+
15+
const markdownWithFrontmatter = `---
16+
title: Example
17+
---
18+
19+
# Workflow
20+
Step 1`;
21+
22+
beforeEach(async () => {
23+
({ testDir, cleanup } = await setupTestDirectory());
24+
vi.spyOn(process, "cwd").mockReturnValue(testDir);
25+
});
26+
27+
afterEach(async () => {
28+
await cleanup();
29+
vi.restoreAllMocks();
30+
});
31+
32+
describe("getSettablePaths", () => {
33+
it("should return workflow path for project mode", () => {
34+
const paths = KiloCommand.getSettablePaths();
35+
36+
expect(paths).toEqual({ relativeDirPath: join(".kilocode", "workflows") });
37+
});
38+
39+
it("should use the same path in global mode", () => {
40+
const paths = KiloCommand.getSettablePaths({ global: true });
41+
42+
expect(paths).toEqual({ relativeDirPath: join(".kilocode", "workflows") });
43+
});
44+
});
45+
46+
describe("toRulesyncCommand", () => {
47+
it("should convert to RulesyncCommand with default frontmatter", () => {
48+
const kiloCommand = new KiloCommand({
49+
baseDir: testDir,
50+
relativeDirPath: ".kilocode/workflows",
51+
relativeFilePath: "test.md",
52+
fileContent: validContent,
53+
validate: true,
54+
});
55+
56+
const rulesyncCommand = kiloCommand.toRulesyncCommand();
57+
58+
expect(rulesyncCommand).toBeInstanceOf(RulesyncCommand);
59+
expect(rulesyncCommand.getFrontmatter()).toEqual({ targets: ["*"], description: "" });
60+
expect(rulesyncCommand.getBody()).toBe(validContent);
61+
expect(rulesyncCommand.getRelativeDirPath()).toBe(RULESYNC_COMMANDS_RELATIVE_DIR_PATH);
62+
});
63+
});
64+
65+
describe("fromRulesyncCommand", () => {
66+
it("should create KiloCommand from RulesyncCommand", () => {
67+
const rulesyncCommand = new RulesyncCommand({
68+
baseDir: testDir,
69+
relativeDirPath: RULESYNC_COMMANDS_RELATIVE_DIR_PATH,
70+
relativeFilePath: "workflow.md",
71+
frontmatter: { targets: ["kilo"], description: "" },
72+
body: validContent,
73+
fileContent: validContent,
74+
validate: true,
75+
});
76+
77+
const kiloCommand = KiloCommand.fromRulesyncCommand({
78+
baseDir: testDir,
79+
rulesyncCommand,
80+
});
81+
82+
expect(kiloCommand).toBeInstanceOf(KiloCommand);
83+
expect(kiloCommand.getRelativeDirPath()).toBe(join(".kilocode", "workflows"));
84+
expect(kiloCommand.getFileContent()).toBe(validContent);
85+
});
86+
});
87+
88+
describe("validate", () => {
89+
it("should always succeed", () => {
90+
const command = new KiloCommand({
91+
baseDir: testDir,
92+
relativeDirPath: ".kilocode/workflows",
93+
relativeFilePath: "test.md",
94+
fileContent: validContent,
95+
validate: true,
96+
});
97+
98+
expect(command.validate()).toEqual({ success: true, error: null });
99+
});
100+
});
101+
102+
describe("fromFile", () => {
103+
it("should load and strip frontmatter", async () => {
104+
const workflowsDir = join(testDir, ".kilocode", "workflows");
105+
const filePath = join(workflowsDir, "workflow.md");
106+
await writeFileContent(filePath, markdownWithFrontmatter);
107+
108+
const command = await KiloCommand.fromFile({
109+
baseDir: testDir,
110+
relativeFilePath: "workflow.md",
111+
});
112+
113+
expect(command).toBeInstanceOf(KiloCommand);
114+
expect(command.getRelativeDirPath()).toBe(join(".kilocode", "workflows"));
115+
expect(command.getFileContent()).toBe("# Workflow\nStep 1");
116+
});
117+
118+
it("should support global workflows", async () => {
119+
const workflowsDir = join(testDir, ".kilocode", "workflows");
120+
const filePath = join(workflowsDir, "global.md");
121+
await writeFileContent(filePath, validContent);
122+
123+
const command = await KiloCommand.fromFile({
124+
baseDir: testDir,
125+
relativeFilePath: "global.md",
126+
global: true,
127+
});
128+
129+
expect(command.getRelativeDirPath()).toBe(join(".kilocode", "workflows"));
130+
expect(command.getFileContent()).toBe(validContent);
131+
});
132+
});
133+
134+
describe("isTargetedByRulesyncCommand", () => {
135+
it("should return true when rulesync targets include kilo", () => {
136+
const rulesyncCommand = new RulesyncCommand({
137+
baseDir: testDir,
138+
relativeDirPath: RULESYNC_COMMANDS_RELATIVE_DIR_PATH,
139+
relativeFilePath: "workflow.md",
140+
frontmatter: { targets: ["kilo"], description: "" },
141+
body: validContent,
142+
fileContent: validContent,
143+
validate: true,
144+
});
145+
146+
expect(KiloCommand.isTargetedByRulesyncCommand(rulesyncCommand)).toBe(true);
147+
});
148+
149+
it("should return false when kilo is not targeted", () => {
150+
const rulesyncCommand = new RulesyncCommand({
151+
baseDir: testDir,
152+
relativeDirPath: RULESYNC_COMMANDS_RELATIVE_DIR_PATH,
153+
relativeFilePath: "workflow.md",
154+
frontmatter: { targets: ["cursor"], description: "" },
155+
body: validContent,
156+
fileContent: validContent,
157+
validate: true,
158+
});
159+
160+
expect(KiloCommand.isTargetedByRulesyncCommand(rulesyncCommand)).toBe(false);
161+
});
162+
});
163+
164+
describe("forDeletion", () => {
165+
it("should create deletable command placeholder", () => {
166+
const command = KiloCommand.forDeletion({
167+
baseDir: testDir,
168+
relativeDirPath: ".kilocode/workflows",
169+
relativeFilePath: "obsolete.md",
170+
});
171+
172+
expect(command.isDeletable()).toBe(true);
173+
expect(command.getFileContent()).toBe("");
174+
});
175+
});
176+
});

0 commit comments

Comments
 (0)