Skip to content

Commit 1e1ec03

Browse files
authored
Merge pull request #875 from devoxx/feature/cli-runners
feat: add Kimi CLI runner
2 parents 3f1b9eb + 2c3fcaa commit 1e1ec03

5 files changed

Lines changed: 194 additions & 14 deletions

File tree

docusaurus/docs/features/cli-runners.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
---
22
sidebar_position: 5
33
title: CLI Runners
4-
description: Execute spec tasks via external CLI tools like Claude Code, GitHub Copilot CLI, OpenAI Codex CLI, or Google Gemini CLI. DevoxxGenie manages the task lifecycle while your preferred tool does the implementation.
5-
keywords: [devoxxgenie, cli runners, claude code, copilot, codex, gemini, cli tools, spec-driven development, sdd, agent loop]
4+
description: Execute spec tasks via external CLI tools like Claude Code, GitHub Copilot CLI, OpenAI Codex CLI, Google Gemini CLI, or Kimi CLI. DevoxxGenie manages the task lifecycle while your preferred tool does the implementation.
5+
keywords: [devoxxgenie, cli runners, claude code, copilot, codex, gemini, kimi, cli tools, spec-driven development, sdd, agent loop]
66
image: /img/devoxxgenie-social-card.jpg
77
---
88

99
# CLI Runners
1010

11-
Instead of using the built-in LLM provider, you can execute spec tasks via **external CLI tools** — such as Claude Code, GitHub Copilot CLI, OpenAI Codex CLI, or Google Gemini CLI. This lets you leverage the tool you already use and trust, while DevoxxGenie manages the task lifecycle.
11+
Instead of using the built-in LLM provider, you can execute spec tasks via **external CLI tools** — such as Claude Code, GitHub Copilot CLI, OpenAI Codex CLI, Google Gemini CLI, or Kimi CLI. This lets you leverage the tool you already use and trust, while DevoxxGenie manages the task lifecycle.
1212

1313
CLI Runners integrate with both [Spec-driven Development](spec-driven-development.md) (single task execution) and the [Agent Loop](sdd-agent-loop.md) (batch task execution).
1414

@@ -35,6 +35,7 @@ Each CLI tool is launched as an external process with your task prompt piped in.
3535
| **GitHub Copilot** | stdin | Auto-generated `--additional-mcp-config` (with `@` prefix) | `--allow-all` |
3636
| **OpenAI Codex** | Trailing argument | Not supported | `exec --model gpt-5.3-codex --full-auto` |
3737
| **Google Gemini** | stdin | Auto-generated `--mcp-config` | *(none)* |
38+
| **Kimi** | `--prompt` flag | Auto-generated `--mcp-config-file` | `--yolo` |
3839
| **Custom** | stdin | Configurable | *(user-defined)* |
3940

4041
:::note
@@ -61,7 +62,7 @@ You can configure **multiple CLI tools** — for example, Claude for complex tas
6162
The **DevoxxGenie Specs** toolbar contains an execution mode dropdown:
6263

6364
- **LLM Provider** — uses the built-in LLM agent (default)
64-
- **CLI: Claude** / **CLI: Copilot** / etc. — uses the configured external CLI tool
65+
- **CLI: Claude** / **CLI: Copilot** / **CLI: Kimi** / etc. — uses the configured external CLI tool
6566

6667
The selection is persisted across IDE restarts. When you click **Run Selected** or **Run All To Do**, tasks are executed using whichever mode is selected.
6768

@@ -71,7 +72,7 @@ The selection is persisted across IDE restarts. When you click **Run Selected**
7172

7273
| Field | Description |
7374
|-------|-------------|
74-
| **Type** | Preset type (Claude, Copilot, Codex, Gemini, Custom). Selecting a type auto-fills the other fields. |
75+
| **Type** | Preset type (Claude, Copilot, Codex, Gemini, Kimi, Custom). Selecting a type auto-fills the other fields. |
7576
| **Executable path** | Absolute path to the CLI binary (e.g., `/opt/homebrew/bin/claude`) |
7677
| **Extra args** | Command-line arguments passed to the CLI. These are split on whitespace — no shell quoting needed. |
7778
| **Env vars** | Optional environment variables as `KEY=VALUE, KEY2=VALUE2`. Useful for API keys not inherited from the shell. |

src/main/java/com/devoxx/genie/model/spec/CliToolConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public enum CliType {
3434
CLAUDE("Claude"),
3535
CODEX("Codex"),
3636
GEMINI("Gemini"),
37+
KIMI("Kimi"),
3738
CUSTOM("Custom");
3839

3940
private final String displayName;
@@ -49,6 +50,7 @@ public enum CliType {
4950
case CLAUDE -> new ClaudeCliCommand();
5051
case CODEX -> new CodexCliCommand();
5152
case GEMINI -> new GeminiCliCommand();
53+
case KIMI -> new KimiCliCommand();
5254
case CUSTOM -> new CustomCliCommand();
5355
};
5456
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.devoxx.genie.service.cli.command;
2+
3+
import com.devoxx.genie.model.spec.CliToolConfig;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.jetbrains.annotations.Nullable;
6+
7+
import java.io.IOException;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
11+
/**
12+
* Kimi CLI: prompt passed via --prompt flag.
13+
* Example: kimi --yolo --mcp-config-file mcp.json --prompt "the prompt here"
14+
*/
15+
public class KimiCliCommand extends AbstractCliCommand {
16+
17+
@Override
18+
public @NotNull List<String> buildProcessCommand(@NotNull CliToolConfig config,
19+
@NotNull String prompt,
20+
@Nullable String mcpConfigPath) {
21+
List<String> command = new ArrayList<>(config.buildCommand());
22+
appendMcpConfig(command, config, mcpConfigPath);
23+
// Kimi takes the prompt via --prompt flag
24+
command.add("--prompt");
25+
command.add(prompt);
26+
return command;
27+
}
28+
29+
@Override
30+
public void writePrompt(@NotNull Process process, @NotNull String prompt) throws IOException {
31+
// Prompt passed as --prompt argument — keep stdin open to avoid BrokenPipeError
32+
}
33+
34+
@Override
35+
public boolean onTaskCompleted(@NotNull Process process) {
36+
// Kimi enters interactive mode after processing — kill it when done
37+
if (process.isAlive()) {
38+
process.destroyForcibly();
39+
return true;
40+
}
41+
return false;
42+
}
43+
44+
@Override
45+
public @NotNull String defaultExecutablePath() {
46+
return "/opt/homebrew/bin/kimi";
47+
}
48+
49+
@Override
50+
public @NotNull String defaultExtraArgs() {
51+
return "--yolo";
52+
}
53+
54+
@Override
55+
public @NotNull String defaultMcpConfigFlag() {
56+
return "--mcp-config-file";
57+
}
58+
}

src/main/java/com/devoxx/genie/ui/settings/spec/SpecSettingsComponent.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -601,10 +601,14 @@ private void runTest() {
601601

602602
com.intellij.openapi.application.ApplicationManager.getApplication().executeOnPooledThread(() -> {
603603
try {
604-
// Build the actual command using the configured flags and pipe a test prompt via stdin
605-
// This verifies authentication, not just installation
604+
// Use the CliCommand abstraction to build command and deliver prompt
605+
// This ensures tool-specific behavior (e.g., --prompt flag for Kimi)
606606
CliToolConfig testConfig = getResult();
607-
java.util.List<String> command = testConfig.buildCommand();
607+
String testPrompt = "Respond with only: OK";
608+
CliToolConfig.CliType cliType = testConfig.getType() != null
609+
? testConfig.getType() : CliToolConfig.CliType.CUSTOM;
610+
com.devoxx.genie.service.cli.command.CliCommand cliCommand = cliType.createCommand();
611+
java.util.List<String> command = cliCommand.buildProcessCommand(testConfig, testPrompt, null);
608612

609613
ProcessBuilder pb = new ProcessBuilder(command);
610614
pb.redirectErrorStream(false);
@@ -619,12 +623,8 @@ private void runTest() {
619623

620624
Process process = pb.start();
621625

622-
// Pipe test prompt via stdin
623-
try (var writer = new java.io.OutputStreamWriter(
624-
process.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8)) {
625-
writer.write("Respond with only: OK");
626-
writer.flush();
627-
}
626+
// Delegate prompt delivery to the command
627+
cliCommand.writePrompt(process, testPrompt);
628628

629629
// Read stdout and stderr in parallel
630630
StringBuilder stdout = new StringBuilder();
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.devoxx.genie.service.cli.command;
2+
3+
import com.devoxx.genie.model.spec.CliToolConfig;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.condition.EnabledIf;
6+
7+
import java.io.BufferedReader;
8+
import java.io.InputStreamReader;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.List;
11+
import java.util.concurrent.TimeUnit;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
/**
16+
* Integration test that verifies Kimi CLI can be launched via ProcessBuilder
17+
* with the --prompt flag. Requires Kimi installed at /Users/stephan/.local/bin/kimi.
18+
*/
19+
class KimiCliCommandIT {
20+
21+
private static final String KIMI_PATH = "/Users/stephan/.local/bin/kimi";
22+
23+
static boolean kimiInstalled() {
24+
return new java.io.File(KIMI_PATH).canExecute();
25+
}
26+
27+
@Test
28+
@EnabledIf("kimiInstalled")
29+
void testBuildProcessCommand() {
30+
KimiCliCommand command = new KimiCliCommand();
31+
CliToolConfig config = CliToolConfig.builder()
32+
.type(CliToolConfig.CliType.KIMI)
33+
.executablePath(KIMI_PATH)
34+
.extraArgs(List.of("--yolo"))
35+
.mcpConfigFlag("--mcp-config-file")
36+
.build();
37+
38+
List<String> cmd = command.buildProcessCommand(config, "say hello", "/tmp/mcp.json");
39+
40+
assertThat(cmd).containsExactly(
41+
KIMI_PATH,
42+
"--yolo",
43+
"--mcp-config-file", "/tmp/mcp.json",
44+
"--prompt", "say hello"
45+
);
46+
}
47+
48+
@Test
49+
@EnabledIf("kimiInstalled")
50+
void testKimiProcessStartsWithPromptFlag() throws Exception {
51+
KimiCliCommand command = new KimiCliCommand();
52+
CliToolConfig config = CliToolConfig.builder()
53+
.type(CliToolConfig.CliType.KIMI)
54+
.executablePath(KIMI_PATH)
55+
.extraArgs(List.of("--yolo"))
56+
.mcpConfigFlag("")
57+
.build();
58+
59+
List<String> cmd = command.buildProcessCommand(config, "Respond with only: OK", null);
60+
61+
ProcessBuilder pb = new ProcessBuilder(cmd);
62+
pb.redirectErrorStream(false);
63+
pb.environment().putAll(System.getenv());
64+
65+
Process process = pb.start();
66+
67+
// Don't close stdin — Kimi crashes with BrokenPipeError if stdin is closed
68+
command.writePrompt(process, "Respond with only: OK");
69+
70+
// Read first few lines of stdout
71+
StringBuilder stdout = new StringBuilder();
72+
Thread stdoutReader = new Thread(() -> {
73+
try (BufferedReader reader = new BufferedReader(
74+
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
75+
String line;
76+
int count = 0;
77+
while ((line = reader.readLine()) != null && count < 20) {
78+
stdout.append(line).append("\n");
79+
count++;
80+
}
81+
} catch (Exception ignored) {}
82+
});
83+
84+
StringBuilder stderr = new StringBuilder();
85+
Thread stderrReader = new Thread(() -> {
86+
try (BufferedReader reader = new BufferedReader(
87+
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
88+
String line;
89+
int count = 0;
90+
while ((line = reader.readLine()) != null && count < 20) {
91+
stderr.append(line).append("\n");
92+
count++;
93+
}
94+
} catch (Exception ignored) {}
95+
});
96+
97+
stdoutReader.setDaemon(true);
98+
stderrReader.setDaemon(true);
99+
stdoutReader.start();
100+
stderrReader.start();
101+
102+
boolean exited = process.waitFor(60, TimeUnit.SECONDS);
103+
if (!exited) {
104+
process.destroyForcibly();
105+
}
106+
107+
stdoutReader.join(3000);
108+
stderrReader.join(3000);
109+
110+
System.out.println("=== STDOUT ===");
111+
System.out.println(stdout);
112+
System.out.println("=== STDERR ===");
113+
System.out.println(stderr);
114+
System.out.println("=== EXIT CODE: " + (exited ? process.exitValue() : "TIMEOUT") + " ===");
115+
116+
// The process should not crash with BrokenPipeError
117+
assertThat(stderr.toString()).doesNotContain("BrokenPipeError");
118+
}
119+
}

0 commit comments

Comments
 (0)