diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7986e72
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+# Maven
+target/
+*.class
+
+# IDE
+.idea/
+*.iml
+.vscode/
+.settings/
+.project
+.classpath
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..a873404
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,116 @@
+
+
+ * 对应 claude-code/src/entrypoints/cli.tsx + * 以 Spring Boot 应用启动,但关闭 Web 服务器(纯 CLI 模式)。 + */ +@SpringBootApplication +public class ClaudeCodeApplication { + + public static void main(String[] args) { + SpringApplication.run(ClaudeCodeApplication.class, args); + } +} diff --git a/src/main/java/com/claudecode/cli/ClaudeCodeRunner.java b/src/main/java/com/claudecode/cli/ClaudeCodeRunner.java new file mode 100644 index 0000000..e9ff323 --- /dev/null +++ b/src/main/java/com/claudecode/cli/ClaudeCodeRunner.java @@ -0,0 +1,30 @@ +package com.claudecode.cli; + +import com.claudecode.repl.ReplSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +/** + * 启动编排器 —— 对应 claude-code/src/main.tsx 的初始化逻辑。 + *
+ * 在 Spring Boot 启动完成后执行,初始化并启动 REPL 会话。
+ */
+@Component
+public class ClaudeCodeRunner implements CommandLineRunner {
+
+ private static final Logger log = LoggerFactory.getLogger(ClaudeCodeRunner.class);
+
+ private final ReplSession replSession;
+
+ public ClaudeCodeRunner(ReplSession replSession) {
+ this.replSession = replSession;
+ }
+
+ @Override
+ public void run(String... args) {
+ log.info("Claude Code (Java) 启动中...");
+ replSession.start();
+ }
+}
diff --git a/src/main/java/com/claudecode/command/CommandContext.java b/src/main/java/com/claudecode/command/CommandContext.java
new file mode 100644
index 0000000..9fc4952
--- /dev/null
+++ b/src/main/java/com/claudecode/command/CommandContext.java
@@ -0,0 +1,17 @@
+package com.claudecode.command;
+
+import com.claudecode.core.AgentLoop;
+import com.claudecode.tool.ToolRegistry;
+
+import java.io.PrintStream;
+
+/**
+ * 命令执行上下文。
+ */
+public record CommandContext(
+ AgentLoop agentLoop,
+ ToolRegistry toolRegistry,
+ PrintStream out,
+ Runnable exitCallback
+) {
+}
diff --git a/src/main/java/com/claudecode/command/CommandRegistry.java b/src/main/java/com/claudecode/command/CommandRegistry.java
new file mode 100644
index 0000000..ac6905c
--- /dev/null
+++ b/src/main/java/com/claudecode/command/CommandRegistry.java
@@ -0,0 +1,66 @@
+package com.claudecode.command;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+
+/**
+ * 命令注册中心 —— 对应 claude-code/src/commands.ts 中的命令集合管理。
+ */
+public class CommandRegistry {
+
+ private static final Logger log = LoggerFactory.getLogger(CommandRegistry.class);
+
+ private final Map
+ * 用于处理以 / 开头的用户输入命令。
+ */
+public interface SlashCommand {
+
+ /** 命令名称(不含 / 前缀) */
+ String name();
+
+ /** 命令描述 */
+ String description();
+
+ /** 命令别名列表 */
+ default List
+ * 集中管理所有组件的创建和依赖注入。
+ */
+@Configuration
+public class AppConfig {
+
+ @Bean
+ public ToolContext toolContext() {
+ return ToolContext.defaultContext();
+ }
+
+ @Bean
+ public ToolRegistry toolRegistry() {
+ ToolRegistry registry = new ToolRegistry();
+ registry.registerAll(
+ new BashTool(),
+ new FileReadTool(),
+ new FileWriteTool(),
+ new FileEditTool(),
+ new GlobTool(),
+ new GrepTool()
+ );
+ return registry;
+ }
+
+ @Bean
+ public CommandRegistry commandRegistry() {
+ CommandRegistry registry = new CommandRegistry();
+ registry.registerAll(
+ new HelpCommand(),
+ new ClearCommand(),
+ new ExitCommand()
+ );
+ return registry;
+ }
+
+ @Bean
+ public String systemPrompt() {
+ Path projectDir = Path.of(System.getProperty("user.dir"));
+ ClaudeMdLoader loader = new ClaudeMdLoader(projectDir);
+ String claudeMd = loader.load();
+
+ return new SystemPromptBuilder()
+ .claudeMd(claudeMd)
+ .build();
+ }
+
+ @Bean
+ public AgentLoop agentLoop(ChatModel chatModel, ToolRegistry toolRegistry,
+ ToolContext toolContext, String systemPrompt) {
+ return new AgentLoop(chatModel, toolRegistry, toolContext, systemPrompt);
+ }
+
+ @Bean
+ public ReplSession replSession(AgentLoop agentLoop, ToolRegistry toolRegistry,
+ CommandRegistry commandRegistry) {
+ return new ReplSession(agentLoop, toolRegistry, commandRegistry);
+ }
+}
diff --git a/src/main/java/com/claudecode/console/AnsiStyle.java b/src/main/java/com/claudecode/console/AnsiStyle.java
new file mode 100644
index 0000000..64a153a
--- /dev/null
+++ b/src/main/java/com/claudecode/console/AnsiStyle.java
@@ -0,0 +1,108 @@
+package com.claudecode.console;
+
+/**
+ * ANSI 终端样式工具类 —— 对应 claude-code/src/utils/terminal.ts 的颜色输出功能。
+ *
+ * 提供终端颜色和样式的便捷方法。
+ */
+public final class AnsiStyle {
+
+ private AnsiStyle() {}
+
+ // 重置
+ public static final String RESET = "\033[0m";
+
+ // 基本样式
+ public static final String BOLD = "\033[1m";
+ public static final String DIM = "\033[2m";
+ public static final String ITALIC = "\033[3m";
+ public static final String UNDERLINE = "\033[4m";
+
+ // 前景色
+ public static final String BLACK = "\033[30m";
+ public static final String RED = "\033[31m";
+ public static final String GREEN = "\033[32m";
+ public static final String YELLOW = "\033[33m";
+ public static final String BLUE = "\033[34m";
+ public static final String MAGENTA = "\033[35m";
+ public static final String CYAN = "\033[36m";
+ public static final String WHITE = "\033[37m";
+
+ // 亮色前景
+ public static final String BRIGHT_BLACK = "\033[90m";
+ public static final String BRIGHT_RED = "\033[91m";
+ public static final String BRIGHT_GREEN = "\033[92m";
+ public static final String BRIGHT_YELLOW = "\033[93m";
+ public static final String BRIGHT_BLUE = "\033[94m";
+ public static final String BRIGHT_MAGENTA = "\033[95m";
+ public static final String BRIGHT_CYAN = "\033[96m";
+ public static final String BRIGHT_WHITE = "\033[97m";
+
+ // 背景色
+ public static final String BG_RED = "\033[41m";
+ public static final String BG_GREEN = "\033[42m";
+ public static final String BG_YELLOW = "\033[43m";
+ public static final String BG_BLUE = "\033[44m";
+
+ // ---- 便捷方法 ----
+
+ public static String bold(String text) {
+ return BOLD + text + RESET;
+ }
+
+ public static String dim(String text) {
+ return DIM + text + RESET;
+ }
+
+ public static String italic(String text) {
+ return ITALIC + text + RESET;
+ }
+
+ public static String red(String text) {
+ return RED + text + RESET;
+ }
+
+ public static String green(String text) {
+ return GREEN + text + RESET;
+ }
+
+ public static String yellow(String text) {
+ return YELLOW + text + RESET;
+ }
+
+ public static String blue(String text) {
+ return BLUE + text + RESET;
+ }
+
+ public static String cyan(String text) {
+ return CYAN + text + RESET;
+ }
+
+ public static String magenta(String text) {
+ return MAGENTA + text + RESET;
+ }
+
+ public static String brightBlack(String text) {
+ return BRIGHT_BLACK + text + RESET;
+ }
+
+ /** 带颜色的前缀标签 */
+ public static String tag(String label, String color) {
+ return color + BOLD + "[" + label + "]" + RESET;
+ }
+
+ /** 带背景色的标签 */
+ public static String badge(String label, String bgColor) {
+ return bgColor + WHITE + BOLD + " " + label + " " + RESET;
+ }
+
+ /** 清除当前行 */
+ public static String clearLine() {
+ return "\033[2K\r";
+ }
+
+ /** 光标上移 n 行 */
+ public static String cursorUp(int n) {
+ return "\033[" + n + "A";
+ }
+}
diff --git a/src/main/java/com/claudecode/console/BannerPrinter.java b/src/main/java/com/claudecode/console/BannerPrinter.java
new file mode 100644
index 0000000..2b0e528
--- /dev/null
+++ b/src/main/java/com/claudecode/console/BannerPrinter.java
@@ -0,0 +1,51 @@
+package com.claudecode.console;
+
+import java.io.PrintStream;
+
+/**
+ * Banner 打印器 —— 对应 claude-code/src/components/Banner.tsx。
+ *
+ * 在启动时打印 ASCII Art Logo 和版本信息。
+ */
+public class BannerPrinter {
+
+ private static final String VERSION = "0.1.0-SNAPSHOT";
+
+ /**
+ * 打印 claude-code-java 启动 banner。
+ */
+ public static void print(PrintStream out) {
+ String banner = """
+ %s
+ ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗
+ ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝
+ ██║ ██║ ███████║██║ ██║██║ ██║█████╗
+ ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝
+ ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗
+ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
+ %s ██████╗ ██████╗ ██████╗ ███████╗
+ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
+ ██║ ██║ ██║██║ ██║█████╗
+ ██║ ██║ ██║██║ ██║██╔══╝
+ ╚██████╗╚██████╔╝██████╔╝███████╗
+ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
+ %s
+ """.formatted(AnsiStyle.BRIGHT_CYAN, AnsiStyle.BRIGHT_MAGENTA, AnsiStyle.RESET);
+
+ out.println(banner);
+ out.println(AnsiStyle.bold(" Claude Code (Java)") + AnsiStyle.dim(" v" + VERSION));
+ out.println(AnsiStyle.dim(" Powered by Spring AI • Type /help for commands"));
+ out.println();
+ }
+
+ /**
+ * 精简版 banner(用于窄终端)。
+ */
+ public static void printCompact(PrintStream out) {
+ out.println();
+ out.println(AnsiStyle.BRIGHT_CYAN + AnsiStyle.BOLD + " ◆ Claude Code (Java)" + AnsiStyle.RESET
+ + AnsiStyle.dim(" v" + VERSION));
+ out.println(AnsiStyle.dim(" Type /help for commands • Ctrl+D to exit"));
+ out.println();
+ }
+}
diff --git a/src/main/java/com/claudecode/console/MarkdownRenderer.java b/src/main/java/com/claudecode/console/MarkdownRenderer.java
new file mode 100644
index 0000000..d901d2a
--- /dev/null
+++ b/src/main/java/com/claudecode/console/MarkdownRenderer.java
@@ -0,0 +1,79 @@
+package com.claudecode.console;
+
+import java.io.PrintStream;
+
+/**
+ * Markdown 简易渲染器 —— 对应 claude-code/src/renderers/markdown.ts。
+ *
+ * 将 AI 回复中的 Markdown 格式转换为终端 ANSI 样式输出。
+ * 这是一个简化版,支持常见格式。
+ */
+public class MarkdownRenderer {
+
+ private final PrintStream out;
+
+ public MarkdownRenderer(PrintStream out) {
+ this.out = out;
+ }
+
+ /** 渲染 Markdown 文本 */
+ public void render(String markdown) {
+ if (markdown == null || markdown.isBlank()) return;
+
+ boolean inCodeBlock = false;
+ String codeBlockLang = "";
+
+ for (String line : markdown.lines().toList()) {
+ // 代码块
+ if (line.stripLeading().startsWith("```")) {
+ if (!inCodeBlock) {
+ codeBlockLang = line.stripLeading().substring(3).strip();
+ inCodeBlock = true;
+ out.println(AnsiStyle.dim(" ┌─" + (codeBlockLang.isEmpty() ? "code" : codeBlockLang) + "─"));
+ continue;
+ } else {
+ inCodeBlock = false;
+ out.println(AnsiStyle.dim(" └─────"));
+ continue;
+ }
+ }
+
+ if (inCodeBlock) {
+ out.println(AnsiStyle.BRIGHT_GREEN + " │ " + line + AnsiStyle.RESET);
+ continue;
+ }
+
+ // 标题
+ if (line.startsWith("### ")) {
+ out.println(AnsiStyle.bold(AnsiStyle.CYAN + " " + line.substring(4)) + AnsiStyle.RESET);
+ } else if (line.startsWith("## ")) {
+ out.println(AnsiStyle.bold(AnsiStyle.BLUE + " " + line.substring(3)) + AnsiStyle.RESET);
+ } else if (line.startsWith("# ")) {
+ out.println(AnsiStyle.bold(AnsiStyle.MAGENTA + " " + line.substring(2)) + AnsiStyle.RESET);
+ }
+ // 列表项
+ else if (line.stripLeading().startsWith("- ") || line.stripLeading().startsWith("* ")) {
+ out.println(" " + AnsiStyle.CYAN + "•" + AnsiStyle.RESET + " " + renderInline(line.stripLeading().substring(2)));
+ }
+ // 分隔线
+ else if (line.strip().matches("^-{3,}$") || line.strip().matches("^\\*{3,}$")) {
+ out.println(AnsiStyle.dim(" ─────────────────────────────────────────"));
+ }
+ // 普通文本
+ else {
+ out.println(" " + renderInline(line));
+ }
+ }
+ }
+
+ /** 行内格式渲染 */
+ private String renderInline(String text) {
+ // 粗体 **text**
+ text = text.replaceAll("\\*\\*(.+?)\\*\\*", AnsiStyle.BOLD + "$1" + AnsiStyle.RESET);
+ // 行内代码 `text`
+ text = text.replaceAll("`(.+?)`", AnsiStyle.BRIGHT_GREEN + "$1" + AnsiStyle.RESET);
+ // 斜体 *text*
+ text = text.replaceAll("\\*(.+?)\\*", AnsiStyle.ITALIC + "$1" + AnsiStyle.RESET);
+ return text;
+ }
+}
diff --git a/src/main/java/com/claudecode/console/SpinnerAnimation.java b/src/main/java/com/claudecode/console/SpinnerAnimation.java
new file mode 100644
index 0000000..dcffe44
--- /dev/null
+++ b/src/main/java/com/claudecode/console/SpinnerAnimation.java
@@ -0,0 +1,72 @@
+package com.claudecode.console;
+
+import java.io.PrintStream;
+
+/**
+ * 加载动画(Spinner)—— 对应 claude-code/src/components/Spinner.tsx。
+ *
+ * 在等待 AI 响应时显示旋转动画。
+ */
+public class SpinnerAnimation {
+
+ private static final String[] FRAMES = {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"};
+ private static final int INTERVAL_MS = 80;
+
+ private final PrintStream out;
+ private volatile boolean running;
+ private Thread thread;
+ private String message = "Thinking";
+
+ public SpinnerAnimation(PrintStream out) {
+ this.out = out;
+ }
+
+ /** 启动 spinner */
+ public void start(String message) {
+ if (running) return;
+ this.message = message;
+ this.running = true;
+
+ thread = Thread.ofVirtual().name("spinner").start(() -> {
+ int idx = 0;
+ while (running) {
+ out.print(AnsiStyle.clearLine());
+ out.print(AnsiStyle.CYAN + " " + FRAMES[idx % FRAMES.length]
+ + " " + AnsiStyle.RESET + AnsiStyle.dim(this.message));
+ out.flush();
+ idx++;
+ try {
+ Thread.sleep(INTERVAL_MS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ // 清除 spinner 行
+ out.print(AnsiStyle.clearLine());
+ out.flush();
+ });
+ }
+
+ /** 停止 spinner */
+ public void stop() {
+ running = false;
+ if (thread != null) {
+ thread.interrupt();
+ try {
+ thread.join(200);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /** 更新消息 */
+ public void updateMessage(String newMessage) {
+ this.message = newMessage;
+ }
+
+ public boolean isRunning() {
+ return running;
+ }
+}
diff --git a/src/main/java/com/claudecode/console/ThinkingRenderer.java b/src/main/java/com/claudecode/console/ThinkingRenderer.java
new file mode 100644
index 0000000..d04d780
--- /dev/null
+++ b/src/main/java/com/claudecode/console/ThinkingRenderer.java
@@ -0,0 +1,43 @@
+package com.claudecode.console;
+
+import java.io.PrintStream;
+
+/**
+ * Thinking 内容渲染器 —— 对应 claude-code/src/components/Thinking.tsx。
+ *
+ * 显示 AI 模型的思考过程(extended thinking)。
+ */
+public class ThinkingRenderer {
+
+ private final PrintStream out;
+
+ public ThinkingRenderer(PrintStream out) {
+ this.out = out;
+ }
+
+ /** 渲染 thinking 内容块 */
+ public void render(String thinkingContent) {
+ if (thinkingContent == null || thinkingContent.isBlank()) {
+ return;
+ }
+
+ out.println();
+ out.println(AnsiStyle.DIM + AnsiStyle.ITALIC + " 💭 Thinking..." + AnsiStyle.RESET);
+
+ // 显示 thinking 内容(缩进并用暗色)
+ for (String line : thinkingContent.lines().toList()) {
+ out.println(AnsiStyle.DIM + " │ " + line + AnsiStyle.RESET);
+ }
+ out.println();
+ }
+
+ /** 渲染 thinking 开始标记 */
+ public void renderStart() {
+ out.print(AnsiStyle.DIM + AnsiStyle.ITALIC + " 💭 Thinking..." + AnsiStyle.RESET);
+ }
+
+ /** 渲染 thinking 结束标记 */
+ public void renderEnd() {
+ out.println(AnsiStyle.clearLine());
+ }
+}
diff --git a/src/main/java/com/claudecode/console/ToolStatusRenderer.java b/src/main/java/com/claudecode/console/ToolStatusRenderer.java
new file mode 100644
index 0000000..5f940f6
--- /dev/null
+++ b/src/main/java/com/claudecode/console/ToolStatusRenderer.java
@@ -0,0 +1,94 @@
+package com.claudecode.console;
+
+import java.io.PrintStream;
+
+/**
+ * 工具调用状态渲染器 —— 对应 claude-code/src/components/ToolStatus.tsx。
+ *
+ * 在终端中显示工具调用的进度和结果。
+ */
+public class ToolStatusRenderer {
+
+ private final PrintStream out;
+
+ public ToolStatusRenderer(PrintStream out) {
+ this.out = out;
+ }
+
+ /** 渲染工具调用开始 */
+ public void renderStart(String toolName, String args) {
+ out.println(AnsiStyle.dim(" ─────────────────────────────────────────"));
+ out.print(AnsiStyle.YELLOW + " ⚙ " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET);
+ out.println(AnsiStyle.dim(" running..."));
+ // 如果有简短参数,显示
+ if (args != null && !args.isBlank()) {
+ String summary = extractSummary(toolName, args);
+ if (summary != null) {
+ out.println(AnsiStyle.dim(" " + summary));
+ }
+ }
+ }
+
+ /** 渲染工具调用完成 */
+ public void renderEnd(String toolName, String result) {
+ // 截断长结果
+ String display = result;
+ if (display != null && display.length() > 500) {
+ display = display.substring(0, 497) + "...";
+ }
+
+ out.println(AnsiStyle.GREEN + " ✓ " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET
+ + AnsiStyle.dim(" done"));
+ if (display != null && !display.isBlank()) {
+ // 缩进输出每一行
+ for (String line : display.lines().toList()) {
+ out.println(AnsiStyle.dim(" " + line));
+ }
+ }
+ out.println(AnsiStyle.dim(" ─────────────────────────────────────────"));
+ }
+
+ /** 渲染工具错误 */
+ public void renderError(String toolName, String error) {
+ out.println(AnsiStyle.RED + " ✗ " + AnsiStyle.BOLD + toolName + AnsiStyle.RESET
+ + AnsiStyle.red(" error"));
+ if (error != null) {
+ out.println(AnsiStyle.red(" " + error));
+ }
+ }
+
+ /** 从 JSON 参数中提取人类可读的摘要 */
+ private String extractSummary(String toolName, String args) {
+ try {
+ // 简单提取关键字段
+ if (args.contains("\"command\"")) {
+ int start = args.indexOf("\"command\"");
+ int valStart = args.indexOf("\"", start + 10) + 1;
+ int valEnd = args.indexOf("\"", valStart);
+ if (valStart > 0 && valEnd > valStart) {
+ String cmd = args.substring(valStart, Math.min(valEnd, valStart + 80));
+ return "$ " + cmd;
+ }
+ }
+ if (args.contains("\"file_path\"")) {
+ int start = args.indexOf("\"file_path\"");
+ int valStart = args.indexOf("\"", start + 12) + 1;
+ int valEnd = args.indexOf("\"", valStart);
+ if (valStart > 0 && valEnd > valStart) {
+ return args.substring(valStart, valEnd);
+ }
+ }
+ if (args.contains("\"pattern\"")) {
+ int start = args.indexOf("\"pattern\"");
+ int valStart = args.indexOf("\"", start + 10) + 1;
+ int valEnd = args.indexOf("\"", valStart);
+ if (valStart > 0 && valEnd > valStart) {
+ return "pattern: " + args.substring(valStart, valEnd);
+ }
+ }
+ } catch (Exception e) {
+ // 忽略解析错误
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/claudecode/context/ClaudeMdLoader.java b/src/main/java/com/claudecode/context/ClaudeMdLoader.java
new file mode 100644
index 0000000..fa9bb27
--- /dev/null
+++ b/src/main/java/com/claudecode/context/ClaudeMdLoader.java
@@ -0,0 +1,91 @@
+package com.claudecode.context;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * CLAUDE.md 加载器 —— 对应 claude-code/src/context.ts 中的 CLAUDE.md 加载逻辑。
+ *
+ * 按优先级从低到高加载:
+ *
+ * 组装完整的系统提示词,包括核心指令、环境信息、工具说明等。
+ */
+public class SystemPromptBuilder {
+
+ private String workDir;
+ private String osName;
+ private String userName;
+ private String claudeMdContent;
+ private String customInstructions;
+
+ public SystemPromptBuilder() {
+ this.workDir = System.getProperty("user.dir");
+ this.osName = System.getProperty("os.name");
+ this.userName = System.getProperty("user.name");
+ }
+
+ public SystemPromptBuilder workDir(String workDir) {
+ this.workDir = workDir;
+ return this;
+ }
+
+ public SystemPromptBuilder claudeMd(String content) {
+ this.claudeMdContent = content;
+ return this;
+ }
+
+ public SystemPromptBuilder customInstructions(String instructions) {
+ this.customInstructions = instructions;
+ return this;
+ }
+
+ /**
+ * 构建完整的系统提示词。
+ */
+ public String build() {
+ StringBuilder sb = new StringBuilder();
+
+ // 核心角色定义
+ sb.append("""
+ You are Claude, an AI assistant made by Anthropic, operating as a CLI coding agent.
+ You are an interactive CLI tool that helps users with software engineering tasks.
+ Use the provided tools to help the user with their request.
+
+ """);
+
+ // 环境信息
+ sb.append("# Environment\n");
+ sb.append("- Working directory: ").append(workDir).append("\n");
+ sb.append("- OS: ").append(osName).append("\n");
+ sb.append("- User: ").append(userName).append("\n");
+ sb.append("\n");
+
+ // 行为准则
+ sb.append("""
+ # Guidelines
+ - Be concise in responses, but thorough in implementation
+ - Always verify changes work before considering a task done
+ - Use tools to explore the codebase before making changes
+ - When writing code, follow existing patterns and conventions
+ - Ask for clarification when requirements are ambiguous
+
+ """);
+
+ // CLAUDE.md 内容
+ if (claudeMdContent != null && !claudeMdContent.isBlank()) {
+ sb.append("# Project Instructions (CLAUDE.md)\n");
+ sb.append(claudeMdContent).append("\n\n");
+ }
+
+ // 自定义指令
+ if (customInstructions != null && !customInstructions.isBlank()) {
+ sb.append("# Custom Instructions\n");
+ sb.append(customInstructions).append("\n\n");
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/claudecode/core/AgentLoop.java b/src/main/java/com/claudecode/core/AgentLoop.java
new file mode 100644
index 0000000..fd3044a
--- /dev/null
+++ b/src/main/java/com/claudecode/core/AgentLoop.java
@@ -0,0 +1,182 @@
+package com.claudecode.core;
+
+import com.claudecode.tool.ToolCallbackAdapter;
+import com.claudecode.tool.ToolContext;
+import com.claudecode.tool.ToolRegistry;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.messages.*;
+import org.springframework.ai.chat.prompt.ChatOptions;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.tool.ToolCallingChatOptions;
+import org.springframework.ai.tool.ToolCallback;
+
+import java.util.*;
+import java.util.function.Consumer;
+
+/**
+ * Agent 循环 —— 对应 claude-code/src/core/query.ts 的 agent loop。
+ *
+ * 使用 ChatModel(非 ChatClient)的显式循环,完整控制每一轮:
+ *
+ * 管理用户输入循环、命令分发、Agent 调用和输出渲染。
+ * 当前版本使用 Scanner 作为输入方式(Phase 2 会升级到 JLine)。
+ */
+public class ReplSession {
+
+ private static final Logger log = LoggerFactory.getLogger(ReplSession.class);
+
+ private final AgentLoop agentLoop;
+ private final ToolRegistry toolRegistry;
+ private final CommandRegistry commandRegistry;
+ private final PrintStream out;
+ private final ToolStatusRenderer toolStatusRenderer;
+ private final MarkdownRenderer markdownRenderer;
+ private final SpinnerAnimation spinner;
+
+ private volatile boolean running = true;
+
+ public ReplSession(AgentLoop agentLoop,
+ ToolRegistry toolRegistry,
+ CommandRegistry commandRegistry) {
+ this.agentLoop = agentLoop;
+ this.toolRegistry = toolRegistry;
+ this.commandRegistry = commandRegistry;
+ this.out = System.out;
+ this.toolStatusRenderer = new ToolStatusRenderer(out);
+ this.markdownRenderer = new MarkdownRenderer(out);
+ this.spinner = new SpinnerAnimation(out);
+
+ // 注册 AgentLoop 事件回调
+ agentLoop.setOnToolEvent(event -> {
+ switch (event.phase()) {
+ case START -> {
+ spinner.stop();
+ toolStatusRenderer.renderStart(event.toolName(), event.arguments());
+ }
+ case END -> {
+ toolStatusRenderer.renderEnd(event.toolName(), event.result());
+ }
+ }
+ });
+
+ agentLoop.setOnAssistantMessage(text -> {
+ // 助手文本在 agent 循环结束后由 REPL 统一渲染
+ });
+ }
+
+ /**
+ * 启动 REPL 主循环。
+ */
+ public void start() {
+ BannerPrinter.printCompact(out);
+ out.println(AnsiStyle.dim(" Working directory: " + System.getProperty("user.dir")));
+ out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered"));
+ out.println();
+
+ Scanner scanner = new Scanner(System.in);
+ CommandContext cmdContext = new CommandContext(agentLoop, toolRegistry, out, () -> running = false);
+
+ while (running) {
+ // 输入提示符
+ out.print(AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN + " ❯ " + AnsiStyle.RESET);
+ out.flush();
+
+ String input;
+ try {
+ if (!scanner.hasNextLine()) {
+ break; // EOF (Ctrl+D)
+ }
+ input = scanner.nextLine().strip();
+ } catch (Exception e) {
+ break;
+ }
+
+ if (input.isEmpty()) {
+ continue;
+ }
+
+ // 检查斜杠命令
+ if (commandRegistry.isCommand(input)) {
+ var result = commandRegistry.dispatch(input, cmdContext);
+ result.ifPresent(out::println);
+ out.println();
+ continue;
+ }
+
+ // 调用 Agent 循环
+ try {
+ spinner.start("Thinking...");
+ String response = agentLoop.run(input);
+ spinner.stop();
+
+ out.println();
+ markdownRenderer.render(response);
+ out.println();
+ } catch (Exception e) {
+ spinner.stop();
+ out.println(AnsiStyle.red("\n ✗ Error: " + e.getMessage()));
+ log.error("Agent 循环异常", e);
+ out.println();
+ }
+ }
+
+ out.println(AnsiStyle.dim("\n Goodbye! 👋\n"));
+ }
+
+ public void stop() {
+ running = false;
+ }
+}
diff --git a/src/main/java/com/claudecode/tool/PermissionResult.java b/src/main/java/com/claudecode/tool/PermissionResult.java
new file mode 100644
index 0000000..43f4f03
--- /dev/null
+++ b/src/main/java/com/claudecode/tool/PermissionResult.java
@@ -0,0 +1,17 @@
+package com.claudecode.tool;
+
+/**
+ * 工具权限检查结果。
+ *
+ * 对应 claude-code 中 Tool.checkPermissions() 的返回值。
+ */
+public record PermissionResult(boolean allowed, String message) {
+
+ /** 放行 */
+ public static final PermissionResult ALLOW = new PermissionResult(true, null);
+
+ /** 拒绝,附带原因 */
+ public static PermissionResult deny(String reason) {
+ return new PermissionResult(false, reason);
+ }
+}
diff --git a/src/main/java/com/claudecode/tool/Tool.java b/src/main/java/com/claudecode/tool/Tool.java
new file mode 100644
index 0000000..a83cf6b
--- /dev/null
+++ b/src/main/java/com/claudecode/tool/Tool.java
@@ -0,0 +1,72 @@
+package com.claudecode.tool;
+
+import java.util.Map;
+
+/**
+ * 工具协议接口 —— 对应 claude-code/src/Tool.ts 中的 Tool 类型定义。
+ *
+ * 每个工具是一个完整的协议实现,包含:
+ *
+ * 示例:
+ *
+ * 将自定义 Tool 协议适配为 Spring AI 的 ToolCallback 接口,
+ * 在调用时处理 JSON 解析、权限检查和异常捕获。
+ *
+ * 对应 claude-code-learn 中的 AgentToolCallback。
+ */
+public class ToolCallbackAdapter implements ToolCallback {
+
+ private static final Logger log = LoggerFactory.getLogger(ToolCallbackAdapter.class);
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private final Tool tool;
+ private final ToolDefinition toolDefinition;
+ private final ToolContext context;
+
+ public ToolCallbackAdapter(Tool tool, ToolContext context) {
+ this.tool = tool;
+ this.context = context;
+ this.toolDefinition = DefaultToolDefinition.builder()
+ .name(tool.name())
+ .description(tool.description())
+ .inputSchema(tool.inputSchema())
+ .build();
+ }
+
+ @Override
+ public ToolDefinition getToolDefinition() {
+ return toolDefinition;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public String call(String jsonInput) {
+ try {
+ Map
+ * 提供工具执行时所需的环境信息和共享状态。
+ */
+public class ToolContext {
+
+ private final Path workDir;
+ private final String model;
+ private final ConcurrentHashMap
+ * 管理 Tool 的注册、查找和到 Spring AI ToolCallback 的转换。
+ */
+public class ToolRegistry {
+
+ private static final Logger log = LoggerFactory.getLogger(ToolRegistry.class);
+
+ private final Map
+ * 在指定工作目录中执行 shell 命令,返回 stdout/stderr 输出。
+ */
+public class BashTool implements Tool {
+
+ /** 默认超时(秒) */
+ private static final int DEFAULT_TIMEOUT = 120;
+
+ @Override
+ public String name() {
+ return "Bash";
+ }
+
+ @Override
+ public String description() {
+ return """
+ Execute a bash command in the working directory. \
+ Use this for file operations, running scripts, installing packages, \
+ or any system command. Commands run in a subprocess with timeout protection.""";
+ }
+
+ @Override
+ public String inputSchema() {
+ return """
+ {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "description": "The shell command to execute"
+ },
+ "timeout": {
+ "type": "integer",
+ "description": "Timeout in seconds (default: 120)"
+ }
+ },
+ "required": ["command"]
+ }""";
+ }
+
+ @Override
+ public String execute(Map
+ * 通过精确匹配 old_string 并替换为 new_string 来编辑文件。
+ */
+public class FileEditTool implements Tool {
+
+ @Override
+ public String name() {
+ return "Edit";
+ }
+
+ @Override
+ public String description() {
+ return """
+ Make a targeted edit to a file by replacing an exact string match with new content. \
+ The old_string must match exactly one location in the file. \
+ Use Read tool first to understand the file content before editing.""";
+ }
+
+ @Override
+ public String inputSchema() {
+ return """
+ {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to the file to edit"
+ },
+ "old_string": {
+ "type": "string",
+ "description": "The exact string to find and replace (must be unique)"
+ },
+ "new_string": {
+ "type": "string",
+ "description": "The replacement string"
+ }
+ },
+ "required": ["file_path", "old_string", "new_string"]
+ }""";
+ }
+
+ @Override
+ public String execute(Map
+ * 读取文件内容,支持行号范围过滤。
+ */
+public class FileReadTool implements Tool {
+
+ /** 单次读取最大行数 */
+ private static final int MAX_LINES = 2000;
+
+ @Override
+ public String name() {
+ return "Read";
+ }
+
+ @Override
+ public String description() {
+ return """
+ Read the contents of a file. Use line_start and line_end to read specific line ranges. \
+ For large files, read in chunks. Supports text files only.""";
+ }
+
+ @Override
+ public String inputSchema() {
+ return """
+ {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Absolute or relative path to the file"
+ },
+ "line_start": {
+ "type": "integer",
+ "description": "Starting line number (1-based, inclusive)"
+ },
+ "line_end": {
+ "type": "integer",
+ "description": "Ending line number (1-based, inclusive)"
+ }
+ },
+ "required": ["file_path"]
+ }""";
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override
+ public String execute(Map
+ * 将内容写入文件(创建或覆盖)。
+ */
+public class FileWriteTool implements Tool {
+
+ @Override
+ public String name() {
+ return "Write";
+ }
+
+ @Override
+ public String description() {
+ return """
+ Write content to a file. Creates the file and any parent directories if they don't exist. \
+ If the file exists, it will be overwritten. Use this for creating new files or completely \
+ replacing file content.""";
+ }
+
+ @Override
+ public String inputSchema() {
+ return """
+ {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Absolute or relative path to the file"
+ },
+ "content": {
+ "type": "string",
+ "description": "The content to write to the file"
+ }
+ },
+ "required": ["file_path", "content"]
+ }""";
+ }
+
+ @Override
+ public String execute(Map
+ * 根据 glob 模式搜索文件。
+ */
+public class GlobTool implements Tool {
+
+ private static final int MAX_RESULTS = 200;
+
+ @Override
+ public String name() {
+ return "Glob";
+ }
+
+ @Override
+ public String description() {
+ return """
+ Find files matching a glob pattern. Returns a list of matching file paths \
+ relative to the working directory. Useful for finding files by name or extension.""";
+ }
+
+ @Override
+ public String inputSchema() {
+ return """
+ {
+ "type": "object",
+ "properties": {
+ "pattern": {
+ "type": "string",
+ "description": "Glob pattern (e.g., '**/*.java', 'src/**/*.ts')"
+ },
+ "path": {
+ "type": "string",
+ "description": "Directory to search in (default: working directory)"
+ }
+ },
+ "required": ["pattern"]
+ }""";
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override
+ public String execute(Map
+ * 在文件中搜索文本模式(正则),优先使用 ripgrep(rg),降级为系统 grep。
+ */
+public class GrepTool implements Tool {
+
+ private static final int MAX_RESULTS = 100;
+
+ @Override
+ public String name() {
+ return "Grep";
+ }
+
+ @Override
+ public String description() {
+ return """
+ Search for a pattern in file contents using regex. Returns matching lines with \
+ file paths and line numbers. Uses ripgrep (rg) if available, falls back to grep.""";
+ }
+
+ @Override
+ public String inputSchema() {
+ return """
+ {
+ "type": "object",
+ "properties": {
+ "pattern": {
+ "type": "string",
+ "description": "Regular expression pattern to search for"
+ },
+ "path": {
+ "type": "string",
+ "description": "Directory or file to search in (default: working directory)"
+ },
+ "include": {
+ "type": "string",
+ "description": "File glob pattern to include (e.g., '*.java')"
+ }
+ },
+ "required": ["pattern"]
+ }""";
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override
+ public String execute(Map
+ *
+ */
+public class ClaudeMdLoader {
+
+ private static final Logger log = LoggerFactory.getLogger(ClaudeMdLoader.class);
+
+ private final Path projectDir;
+
+ public ClaudeMdLoader(Path projectDir) {
+ this.projectDir = projectDir;
+ }
+
+ /**
+ * 加载并合并所有 CLAUDE.md 内容。
+ */
+ public String load() {
+ List
+ *
+ */
+public class AgentLoop {
+
+ private static final Logger log = LoggerFactory.getLogger(AgentLoop.class);
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ /** 单轮最大迭代次数,防止无限循环 */
+ private static final int MAX_ITERATIONS = 50;
+
+ private final ChatModel chatModel;
+ private final ToolRegistry toolRegistry;
+ private final ToolContext toolContext;
+ private final String systemPrompt;
+
+ /** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */
+ private final List
+ *
+ */
+public interface Tool {
+
+ /** 工具唯一名称标识 */
+ String name();
+
+ /** 给 LLM 看的工具描述 */
+ String description();
+
+ /**
+ * 输入参数的 JSON Schema 定义。
+ * {@code
+ * {
+ * "type": "object",
+ * "properties": {
+ * "command": { "type": "string", "description": "Shell command to execute" }
+ * },
+ * "required": ["command"]
+ * }
+ * }
+ */
+ String inputSchema();
+
+ /**
+ * 执行工具。
+ *
+ * @param input JSON 解析后的输入参数
+ * @param context 执行上下文(工作目录、会话状态等)
+ * @return 执行结果文本
+ */
+ String execute(Map