diff --git a/src/main/java/com/claudecode/command/impl/CompactCommand.java b/src/main/java/com/claudecode/command/impl/CompactCommand.java index 70bfdac..b8456ee 100644 --- a/src/main/java/com/claudecode/command/impl/CompactCommand.java +++ b/src/main/java/com/claudecode/command/impl/CompactCommand.java @@ -3,12 +3,13 @@ package com.claudecode.command.impl; import com.claudecode.command.CommandContext; import com.claudecode.command.SlashCommand; import com.claudecode.console.AnsiStyle; +import com.claudecode.core.TokenTracker; /** * /compact 命令 —— 压缩当前对话上下文。 *
* 对应 claude-code/src/commands/compact.ts。 - * 当前为简化实现,直接清空历史并提示用户。 + * 保留系统提示词,用摘要替换详细的对话历史。 */ public class CompactCommand implements SlashCommand { @@ -24,11 +25,33 @@ public class CompactCommand implements SlashCommand { @Override public String execute(String args, CommandContext context) { - if (context.agentLoop() != null) { - int before = context.agentLoop().getMessageHistory().size(); - context.agentLoop().reset(); - return AnsiStyle.green(" ✓ Context compacted: " + before + " messages → 1 (system prompt only)"); + if (context.agentLoop() == null) { + return AnsiStyle.yellow(" ⚠ No active conversation to compact."); } - return AnsiStyle.yellow(" ⚠ No active conversation to compact."); + + int before = context.agentLoop().getMessageHistory().size(); + + if (before <= 2) { + return AnsiStyle.dim(" Context is already minimal (" + before + " messages). Nothing to compact."); + } + + // 记录压缩前的 token 使用 + TokenTracker tracker = context.agentLoop().getTokenTracker(); + long tokensBefore = tracker.getInputTokens() + tracker.getOutputTokens(); + + // 重置历史(保留系统提示词) + context.agentLoop().reset(); + + int after = context.agentLoop().getMessageHistory().size(); + + StringBuilder sb = new StringBuilder(); + sb.append(AnsiStyle.green(" ✅ Context compacted")).append("\n"); + sb.append(" Messages: ").append(before).append(" → ").append(after).append("\n"); + if (tokensBefore > 0) { + sb.append(" Tokens used before compact: ").append(TokenTracker.formatTokens(tokensBefore)).append("\n"); + } + sb.append(AnsiStyle.dim(" Conversation history cleared. System prompt retained.")); + + return sb.toString(); } } diff --git a/src/main/java/com/claudecode/command/impl/ConfigCommand.java b/src/main/java/com/claudecode/command/impl/ConfigCommand.java new file mode 100644 index 0000000..0186ab1 --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/ConfigCommand.java @@ -0,0 +1,124 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +import java.util.List; +import java.util.Map; + +/** + * /config 命令 —— 查看和设置应用配置。 + *
+ * 支持查看当前配置、设置单个配置项。
+ * 配置变更仅在当前会话内生效。
+ */
+public class ConfigCommand implements SlashCommand {
+
+ /** 支持的配置项及说明 */
+ private static final Map
+ * 展示已加载的 CLAUDE.md、Skills、Git 上下文和 Token 预算使用情况。
+ */
+public class ContextCommand implements SlashCommand {
+
+ @Override
+ public String name() {
+ return "context";
+ }
+
+ @Override
+ public String description() {
+ return "Show current context (CLAUDE.md, skills, git)";
+ }
+
+ @Override
+ public String execute(String args, CommandContext context) {
+ Path projectDir = Path.of(System.getProperty("user.dir"));
+ StringBuilder sb = new StringBuilder();
+ sb.append("\n");
+ sb.append(AnsiStyle.bold(" 📋 Context Overview\n"));
+ sb.append(" ").append("─".repeat(40)).append("\n\n");
+
+ // CLAUDE.md 状态
+ sb.append(AnsiStyle.bold(" CLAUDE.md:\n"));
+ ClaudeMdLoader loader = new ClaudeMdLoader(projectDir);
+ String claudeMd = loader.load();
+ if (claudeMd.isEmpty()) {
+ sb.append(AnsiStyle.dim(" (none loaded) — run /init to create one\n"));
+ } else {
+ // 显示摘要(前 200 字符)
+ int lines = claudeMd.split("\n").length;
+ sb.append(" ").append(AnsiStyle.green(lines + " lines loaded")).append("\n");
+ String preview = claudeMd.length() > 200
+ ? claudeMd.substring(0, 200) + "..."
+ : claudeMd;
+ for (String line : preview.split("\n")) {
+ sb.append(AnsiStyle.dim(" │ " + line)).append("\n");
+ }
+ }
+ sb.append("\n");
+
+ // Skills 状态
+ sb.append(AnsiStyle.bold(" Skills:\n"));
+ SkillLoader skillLoader = new SkillLoader(projectDir);
+ var skills = skillLoader.loadAll();
+ if (skills.isEmpty()) {
+ sb.append(AnsiStyle.dim(" (none loaded) — add .md files to .claude/skills/\n"));
+ } else {
+ for (var skill : skills) {
+ sb.append(" • ").append(AnsiStyle.cyan(skill.name()));
+ if (!skill.description().isEmpty()) {
+ sb.append(AnsiStyle.dim(" — " + skill.description()));
+ }
+ sb.append(AnsiStyle.dim(" [" + skill.source() + "]")).append("\n");
+ }
+ }
+ sb.append("\n");
+
+ // Git 上下文
+ sb.append(AnsiStyle.bold(" Git:\n"));
+ GitContext git = new GitContext(projectDir).collect();
+ if (!git.isGitRepo()) {
+ sb.append(AnsiStyle.dim(" (not a git repository)\n"));
+ } else {
+ sb.append(" Branch: ").append(AnsiStyle.cyan(git.getBranch() != null ? git.getBranch() : "unknown")).append("\n");
+ if (git.getStatus() != null) {
+ long modifiedCount = git.getStatus().lines()
+ .filter(l -> !l.startsWith("##"))
+ .count();
+ if (modifiedCount > 0) {
+ sb.append(" Modified: ").append(AnsiStyle.yellow(modifiedCount + " file(s)")).append("\n");
+ } else {
+ sb.append(" Working tree: ").append(AnsiStyle.green("clean")).append("\n");
+ }
+ }
+ }
+ sb.append("\n");
+
+ // Token 预算
+ sb.append(AnsiStyle.bold(" System Prompt:\n"));
+ String sysPrompt = context.agentLoop().getSystemPrompt();
+ int charCount = sysPrompt.length();
+ int estimatedTokens = charCount / 4; // 粗略估算
+ sb.append(" Size: ").append(charCount).append(" chars (~").append(estimatedTokens).append(" tokens)\n");
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/claudecode/command/impl/CostCommand.java b/src/main/java/com/claudecode/command/impl/CostCommand.java
index e39464a..e3fde13 100644
--- a/src/main/java/com/claudecode/command/impl/CostCommand.java
+++ b/src/main/java/com/claudecode/command/impl/CostCommand.java
@@ -3,12 +3,13 @@ package com.claudecode.command.impl;
import com.claudecode.command.CommandContext;
import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle;
+import com.claudecode.core.TokenTracker;
/**
* /cost 命令 —— 显示 Token 使用量和费用估算。
*
* 对应 claude-code/src/commands/cost.ts。
- * 当前为占位实现,后续接入实际 Token 统计。
+ * 从 AgentLoop 的 TokenTracker 获取真实 Token 统计。
*/
public class CostCommand implements SlashCommand {
@@ -24,21 +25,40 @@ public class CostCommand implements SlashCommand {
@Override
public String execute(String args, CommandContext context) {
- int msgCount = 0;
- if (context.agentLoop() != null) {
- msgCount = context.agentLoop().getMessageHistory().size();
- }
+ TokenTracker tracker = context.agentLoop().getTokenTracker();
+ int msgCount = context.agentLoop().getMessageHistory().size();
StringBuilder sb = new StringBuilder();
sb.append("\n");
- sb.append(AnsiStyle.bold(" Token Usage:\n\n"));
- sb.append(" Messages: ").append(AnsiStyle.cyan(String.valueOf(msgCount))).append("\n");
- sb.append(" Input tokens: ").append(AnsiStyle.dim("(tracking not yet implemented)")).append("\n");
- sb.append(" Output tokens:").append(AnsiStyle.dim("(tracking not yet implemented)")).append("\n");
- sb.append(" Est. cost: ").append(AnsiStyle.dim("(tracking not yet implemented)")).append("\n");
- sb.append("\n");
- sb.append(AnsiStyle.dim(" Token tracking will be added in a future update."));
+ sb.append(AnsiStyle.bold(" 💰 Token Usage & Cost\n"));
+ sb.append(" ").append("─".repeat(40)).append("\n\n");
+
+ sb.append(" ").append(AnsiStyle.bold("Model: ")).append(AnsiStyle.cyan(tracker.getModelName())).append("\n");
+ sb.append(" ").append(AnsiStyle.bold("API Calls: ")).append(tracker.getApiCallCount()).append("\n");
+ sb.append(" ").append(AnsiStyle.bold("Messages: ")).append(msgCount).append("\n\n");
+
+ sb.append(" ").append(AnsiStyle.bold("Input tokens: ")).append(formatTokenLine(tracker.getInputTokens())).append("\n");
+ sb.append(" ").append(AnsiStyle.bold("Output tokens:")).append(formatTokenLine(tracker.getOutputTokens())).append("\n");
+
+ if (tracker.getCacheReadTokens() > 0) {
+ sb.append(" ").append(AnsiStyle.bold("Cache read: ")).append(formatTokenLine(tracker.getCacheReadTokens())).append("\n");
+ }
+ if (tracker.getCacheCreationTokens() > 0) {
+ sb.append(" ").append(AnsiStyle.bold("Cache create: ")).append(formatTokenLine(tracker.getCacheCreationTokens())).append("\n");
+ }
+
+ sb.append(" ").append("─".repeat(30)).append("\n");
+ sb.append(" ").append(AnsiStyle.bold("Total: ")).append(TokenTracker.formatTokens(tracker.getTotalTokens())).append(" tokens\n");
+ sb.append(" ").append(AnsiStyle.bold("Est. Cost: ")).append(AnsiStyle.green("$" + String.format("%.4f", tracker.estimateCost()))).append("\n");
+
+ if (tracker.getApiCallCount() == 0) {
+ sb.append("\n").append(AnsiStyle.dim(" No API calls yet. Start a conversation to see usage."));
+ }
return sb.toString();
}
+
+ private String formatTokenLine(long tokens) {
+ return " " + TokenTracker.formatTokens(tokens) + AnsiStyle.dim(" (" + tokens + ")");
+ }
}
diff --git a/src/main/java/com/claudecode/command/impl/InitCommand.java b/src/main/java/com/claudecode/command/impl/InitCommand.java
new file mode 100644
index 0000000..20d0cbc
--- /dev/null
+++ b/src/main/java/com/claudecode/command/impl/InitCommand.java
@@ -0,0 +1,140 @@
+package com.claudecode.command.impl;
+
+import com.claudecode.command.CommandContext;
+import com.claudecode.command.SlashCommand;
+import com.claudecode.console.AnsiStyle;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * /init 命令 —— 初始化项目 CLAUDE.md。
+ *
+ * 对应 claude-code/src/commands/init.ts。
+ * 检测项目类型并生成 CLAUDE.md 模板文件。
+ */
+public class InitCommand implements SlashCommand {
+
+ @Override
+ public String name() {
+ return "init";
+ }
+
+ @Override
+ public String description() {
+ return "Initialize CLAUDE.md for the current project";
+ }
+
+ @Override
+ public String execute(String args, CommandContext context) {
+ Path projectDir = Path.of(System.getProperty("user.dir"));
+ Path claudeMdPath = projectDir.resolve("CLAUDE.md");
+
+ // 检查是否已存在
+ if (Files.exists(claudeMdPath)) {
+ return AnsiStyle.yellow(" ⚠ CLAUDE.md already exists at: " + claudeMdPath) + "\n"
+ + AnsiStyle.dim(" Use a text editor to modify it, or delete and re-run /init.");
+ }
+
+ // 检测项目类型
+ ProjectType type = detectProjectType(projectDir);
+
+ // 生成 CLAUDE.md 内容
+ String content = generateClaudeMd(projectDir, type);
+
+ try {
+ Files.writeString(claudeMdPath, content, StandardCharsets.UTF_8);
+
+ // 如果存在 .claude 目录,也创建 skills 目录
+ Path claudeDir = projectDir.resolve(".claude");
+ Path skillsDir = claudeDir.resolve("skills");
+ if (!Files.exists(skillsDir)) {
+ Files.createDirectories(skillsDir);
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(AnsiStyle.green(" ✅ Created CLAUDE.md")).append("\n");
+ sb.append(AnsiStyle.dim(" Path: " + claudeMdPath)).append("\n");
+ sb.append(AnsiStyle.dim(" Project type: " + type.displayName)).append("\n");
+ sb.append(AnsiStyle.dim(" Skills dir: " + skillsDir)).append("\n\n");
+ sb.append(AnsiStyle.dim(" Edit CLAUDE.md to customize instructions for the AI assistant."));
+ return sb.toString();
+
+ } catch (IOException e) {
+ return AnsiStyle.red(" ✗ Failed to create CLAUDE.md: " + e.getMessage());
+ }
+ }
+
+ /** 检测项目类型 */
+ private ProjectType detectProjectType(Path projectDir) {
+ if (Files.exists(projectDir.resolve("pom.xml"))) return ProjectType.MAVEN;
+ if (Files.exists(projectDir.resolve("build.gradle")) || Files.exists(projectDir.resolve("build.gradle.kts")))
+ return ProjectType.GRADLE;
+ if (Files.exists(projectDir.resolve("package.json"))) return ProjectType.NODE;
+ if (Files.exists(projectDir.resolve("pyproject.toml")) || Files.exists(projectDir.resolve("setup.py")))
+ return ProjectType.PYTHON;
+ if (Files.exists(projectDir.resolve("Cargo.toml"))) return ProjectType.RUST;
+ if (Files.exists(projectDir.resolve("go.mod"))) return ProjectType.GO;
+ if (Files.exists(projectDir.resolve("Gemfile"))) return ProjectType.RUBY;
+ return ProjectType.GENERIC;
+ }
+
+ /** 生成 CLAUDE.md 内容 */
+ private String generateClaudeMd(Path projectDir, ProjectType type) {
+ String projectName = projectDir.getFileName().toString();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("# ").append(projectName).append("\n\n");
+ sb.append("## Project Overview\n\n");
+ sb.append("\n\n");
+
+ sb.append("## Tech Stack\n\n");
+ sb.append(type.techStackHint).append("\n\n");
+
+ sb.append("## Build & Run\n\n");
+ sb.append("```bash\n");
+ sb.append(type.buildCommand).append("\n");
+ sb.append("```\n\n");
+
+ sb.append("## Test\n\n");
+ sb.append("```bash\n");
+ sb.append(type.testCommand).append("\n");
+ sb.append("```\n\n");
+
+ sb.append("## Code Style\n\n");
+ sb.append("- Follow existing patterns in the codebase\n");
+ sb.append("- Write clear, descriptive variable names\n");
+ sb.append("- Add comments for complex business logic\n\n");
+
+ sb.append("## Project Structure\n\n");
+ sb.append("\n");
+
+ return sb.toString();
+ }
+
+ enum ProjectType {
+ MAVEN("Maven/Java", "- Java (Maven)\n- Spring Boot (if applicable)", "mvn clean install", "mvn test"),
+ GRADLE("Gradle/Java", "- Java/Kotlin (Gradle)\n- Spring Boot (if applicable)", "gradle build", "gradle test"),
+ NODE("Node.js", "- Node.js\n- TypeScript/JavaScript", "npm install && npm run build", "npm test"),
+ PYTHON("Python", "- Python 3\n- pip/poetry", "pip install -e .", "pytest"),
+ RUST("Rust", "- Rust\n- Cargo", "cargo build", "cargo test"),
+ GO("Go", "- Go", "go build ./...", "go test ./..."),
+ RUBY("Ruby", "- Ruby\n- Bundler", "bundle install", "bundle exec rspec"),
+ GENERIC("Generic", "", "# add build command", "# add test command");
+
+ final String displayName;
+ final String techStackHint;
+ final String buildCommand;
+ final String testCommand;
+
+ ProjectType(String displayName, String techStackHint, String buildCommand, String testCommand) {
+ this.displayName = displayName;
+ this.techStackHint = techStackHint;
+ this.buildCommand = buildCommand;
+ this.testCommand = testCommand;
+ }
+ }
+}
diff --git a/src/main/java/com/claudecode/command/impl/ModelCommand.java b/src/main/java/com/claudecode/command/impl/ModelCommand.java
index 0b862bf..c981ebc 100644
--- a/src/main/java/com/claudecode/command/impl/ModelCommand.java
+++ b/src/main/java/com/claudecode/command/impl/ModelCommand.java
@@ -5,12 +5,22 @@ import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle;
import java.util.List;
+import java.util.Map;
/**
* /model 命令 —— 显示或切换当前 AI 模型。
+ *
+ * 支持查看当前模型信息和切换到其他模型。
*/
public class ModelCommand implements SlashCommand {
+ private static final Map
+ * 展示当前模型、Token 用量、工具数、消息数、内存和运行时间等信息。
+ */
+public class StatusCommand implements SlashCommand {
+
+ private final Instant startTime = Instant.now();
+
+ @Override
+ public String name() {
+ return "status";
+ }
+
+ @Override
+ public String description() {
+ return "Show session status dashboard";
+ }
+
+ @Override
+ public String execute(String args, CommandContext context) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("\n");
+ sb.append(AnsiStyle.bold(" 📊 Session Status\n"));
+ sb.append(" ").append("─".repeat(40)).append("\n\n");
+
+ // 模型信息
+ TokenTracker tracker = context.agentLoop().getTokenTracker();
+ sb.append(" ").append(AnsiStyle.bold("Model: ")).append(AnsiStyle.cyan(tracker.getModelName())).append("\n");
+
+ // Token 使用
+ sb.append(" ").append(AnsiStyle.bold("Tokens: "))
+ .append("↑ ").append(TokenTracker.formatTokens(tracker.getInputTokens()))
+ .append(" input, ↓ ").append(TokenTracker.formatTokens(tracker.getOutputTokens()))
+ .append(" output")
+ .append(AnsiStyle.dim(" ($" + String.format("%.4f", tracker.estimateCost()) + ")"))
+ .append("\n");
+
+ // API 调用次数
+ sb.append(" ").append(AnsiStyle.bold("API Calls:")).append(" ").append(tracker.getApiCallCount()).append("\n");
+
+ // 消息历史
+ int msgCount = context.agentLoop().getMessageHistory().size();
+ sb.append(" ").append(AnsiStyle.bold("Messages: ")).append(msgCount).append("\n");
+
+ // 工具数
+ sb.append(" ").append(AnsiStyle.bold("Tools: ")).append(context.toolRegistry().size()).append(" registered\n");
+
+ // 工作目录
+ sb.append(" ").append(AnsiStyle.bold("Work Dir: ")).append(AnsiStyle.dim(System.getProperty("user.dir"))).append("\n");
+
+ // JVM 内存
+ Runtime rt = Runtime.getRuntime();
+ long usedMB = (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
+ long maxMB = rt.maxMemory() / (1024 * 1024);
+ sb.append(" ").append(AnsiStyle.bold("Memory: ")).append(usedMB).append("MB / ").append(maxMB).append("MB\n");
+
+ // 运行时间
+ Duration uptime = Duration.between(startTime, Instant.now());
+ sb.append(" ").append(AnsiStyle.bold("Uptime: ")).append(formatDuration(uptime)).append("\n");
+
+ // Java 版本
+ sb.append(" ").append(AnsiStyle.bold("JDK: ")).append(System.getProperty("java.version")).append("\n");
+
+ return sb.toString();
+ }
+
+ private String formatDuration(Duration d) {
+ long hours = d.toHours();
+ long minutes = d.toMinutesPart();
+ long seconds = d.toSecondsPart();
+ if (hours > 0) return String.format("%dh %dm %ds", hours, minutes, seconds);
+ if (minutes > 0) return String.format("%dm %ds", minutes, seconds);
+ return String.format("%ds", seconds);
+ }
+}
diff --git a/src/main/java/com/claudecode/config/AppConfig.java b/src/main/java/com/claudecode/config/AppConfig.java
index ba13b4c..a736339 100644
--- a/src/main/java/com/claudecode/config/AppConfig.java
+++ b/src/main/java/com/claudecode/config/AppConfig.java
@@ -1,15 +1,13 @@
package com.claudecode.config;
import com.claudecode.command.CommandRegistry;
-import com.claudecode.command.impl.ClearCommand;
-import com.claudecode.command.impl.CompactCommand;
-import com.claudecode.command.impl.CostCommand;
-import com.claudecode.command.impl.ExitCommand;
-import com.claudecode.command.impl.HelpCommand;
-import com.claudecode.command.impl.ModelCommand;
+import com.claudecode.command.impl.*;
import com.claudecode.context.ClaudeMdLoader;
+import com.claudecode.context.GitContext;
+import com.claudecode.context.SkillLoader;
import com.claudecode.context.SystemPromptBuilder;
import com.claudecode.core.AgentLoop;
+import com.claudecode.core.TokenTracker;
import com.claudecode.repl.ReplSession;
import com.claudecode.tool.ToolContext;
import com.claudecode.tool.ToolRegistry;
@@ -62,26 +60,51 @@ public class AppConfig {
new CompactCommand(),
new CostCommand(),
new ModelCommand(),
+ new StatusCommand(),
+ new ContextCommand(),
+ new InitCommand(),
+ new ConfigCommand(),
new ExitCommand()
);
return registry;
}
+ @Bean
+ public TokenTracker tokenTracker() {
+ TokenTracker tracker = new TokenTracker();
+ String model = System.getenv().getOrDefault("AI_MODEL", "claude-sonnet-4-20250514");
+ tracker.setModel(model);
+ return tracker;
+ }
+
@Bean
public String systemPrompt() {
Path projectDir = Path.of(System.getProperty("user.dir"));
- ClaudeMdLoader loader = new ClaudeMdLoader(projectDir);
- String claudeMd = loader.load();
+
+ // 加载 CLAUDE.md
+ ClaudeMdLoader claudeLoader = new ClaudeMdLoader(projectDir);
+ String claudeMd = claudeLoader.load();
+
+ // 加载 Skills
+ SkillLoader skillLoader = new SkillLoader(projectDir);
+ skillLoader.loadAll();
+ String skillsSummary = skillLoader.buildSkillsSummary();
+
+ // 收集 Git 上下文
+ GitContext gitContext = new GitContext(projectDir).collect();
+ String gitSummary = gitContext.buildSummary();
return new SystemPromptBuilder()
.claudeMd(claudeMd)
+ .skills(skillsSummary)
+ .git(gitSummary)
.build();
}
@Bean
public AgentLoop agentLoop(@Qualifier("anthropicChatModel") ChatModel chatModel, ToolRegistry toolRegistry,
- ToolContext toolContext, String systemPrompt) {
- AgentLoop mainLoop = new AgentLoop(chatModel, toolRegistry, toolContext, systemPrompt);
+ ToolContext toolContext, String systemPrompt, TokenTracker tokenTracker) {
+ AgentLoop mainLoop = new AgentLoop(chatModel, toolRegistry, toolContext, systemPrompt, tokenTracker);
// 注册子 Agent 工厂到 ToolContext,使 AgentTool 能创建独立的 AgentLoop
toolContext.set(AgentTool.AGENT_FACTORY_KEY,
diff --git a/src/main/java/com/claudecode/context/GitContext.java b/src/main/java/com/claudecode/context/GitContext.java
new file mode 100644
index 0000000..cf74538
--- /dev/null
+++ b/src/main/java/com/claudecode/context/GitContext.java
@@ -0,0 +1,127 @@
+package com.claudecode.context;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Git 上下文收集器 —— 收集当前项目的 Git 信息。
+ *
+ * 提取分支名、状态、最近提交等信息,注入系统提示词中,
+ * 帮助 AI 理解当前代码版本和变更状态。
+ */
+public class GitContext {
+
+ private static final Logger log = LoggerFactory.getLogger(GitContext.class);
+ private static final int CMD_TIMEOUT = 5; // 秒
+
+ private final Path projectDir;
+ private String branch;
+ private String status;
+ private String recentCommits;
+ private boolean isGitRepo;
+
+ public GitContext(Path projectDir) {
+ this.projectDir = projectDir;
+ this.isGitRepo = Files.isDirectory(projectDir.resolve(".git"));
+ }
+
+ /**
+ * 收集 Git 上下文信息
+ */
+ public GitContext collect() {
+ if (!isGitRepo) {
+ log.debug("当前目录不是 Git 仓库: {}", projectDir);
+ return this;
+ }
+
+ this.branch = runGitCommand("rev-parse", "--abbrev-ref", "HEAD");
+ this.status = runGitCommand("status", "--short", "--branch");
+ this.recentCommits = runGitCommand("log", "--oneline", "-5", "--no-decorate");
+
+ return this;
+ }
+
+ /**
+ * 构建 Git 上下文摘要(注入系统提示词)
+ */
+ public String buildSummary() {
+ if (!isGitRepo) {
+ return "";
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("# Git Context\n\n");
+
+ if (branch != null && !branch.isBlank()) {
+ sb.append("- Branch: ").append(branch).append("\n");
+ }
+
+ if (status != null && !status.isBlank()) {
+ // 提取修改文件数
+ long modifiedCount = status.lines()
+ .filter(l -> !l.startsWith("##"))
+ .count();
+ if (modifiedCount > 0) {
+ sb.append("- Modified files: ").append(modifiedCount).append("\n");
+ sb.append("- Status:\n```\n").append(status).append("\n```\n");
+ } else {
+ sb.append("- Working tree: clean\n");
+ }
+ }
+
+ if (recentCommits != null && !recentCommits.isBlank()) {
+ sb.append("- Recent commits:\n```\n").append(recentCommits).append("\n```\n");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * 执行 Git 命令
+ */
+ private String runGitCommand(String... args) {
+ try {
+ var command = new java.util.ArrayList
+ * 从多个来源扫描和加载 .md 格式的技能文件:
+ *
+ * 每个技能文件支持 YAML frontmatter 元数据:
+ *
- * 组装完整的系统提示词,包括核心指令、环境信息、工具说明等。
+ * 组装完整的系统提示词,包括核心指令、环境信息、
+ * CLAUDE.md、Skills、Git 上下文等模块化内容。
*/
public class SystemPromptBuilder {
@@ -12,6 +13,8 @@ public class SystemPromptBuilder {
private String userName;
private String claudeMdContent;
private String customInstructions;
+ private String skillsSummary;
+ private String gitSummary;
public SystemPromptBuilder() {
this.workDir = System.getProperty("user.dir");
@@ -34,6 +37,16 @@ public class SystemPromptBuilder {
return this;
}
+ public SystemPromptBuilder skills(String skillsSummary) {
+ this.skillsSummary = skillsSummary;
+ return this;
+ }
+
+ public SystemPromptBuilder git(String gitSummary) {
+ this.gitSummary = gitSummary;
+ return this;
+ }
+
/**
* 构建完整的系统提示词。
*/
@@ -53,6 +66,8 @@ public class SystemPromptBuilder {
sb.append("- Working directory: ").append(workDir).append("\n");
sb.append("- OS: ").append(osName).append("\n");
sb.append("- User: ").append(userName).append("\n");
+ sb.append("- Shell: ").append(System.getenv().getOrDefault("SHELL",
+ System.getenv().getOrDefault("COMSPEC", "unknown"))).append("\n");
sb.append("\n");
// 行为准则
@@ -63,15 +78,27 @@ public class SystemPromptBuilder {
- Use tools to explore the codebase before making changes
- When writing code, follow existing patterns and conventions
- Ask for clarification when requirements are ambiguous
+ - When making file edits, always use the Edit tool with exact string matching
+ - Prefer editing existing files over creating new ones
""");
+ // Git 上下文
+ if (gitSummary != null && !gitSummary.isBlank()) {
+ sb.append(gitSummary).append("\n");
+ }
+
// CLAUDE.md 内容
if (claudeMdContent != null && !claudeMdContent.isBlank()) {
sb.append("# Project Instructions (CLAUDE.md)\n");
sb.append(claudeMdContent).append("\n\n");
}
+ // Skills 摘要
+ if (skillsSummary != null && !skillsSummary.isBlank()) {
+ sb.append(skillsSummary).append("\n");
+ }
+
// 自定义指令
if (customInstructions != null && !customInstructions.isBlank()) {
sb.append("# Custom Instructions\n");
diff --git a/src/main/java/com/claudecode/core/AgentLoop.java b/src/main/java/com/claudecode/core/AgentLoop.java
index fd3044a..5f8275b 100644
--- a/src/main/java/com/claudecode/core/AgentLoop.java
+++ b/src/main/java/com/claudecode/core/AgentLoop.java
@@ -41,6 +41,7 @@ public class AgentLoop {
private final ToolRegistry toolRegistry;
private final ToolContext toolContext;
private final String systemPrompt;
+ private final TokenTracker tokenTracker;
/** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */
private final List
+ * 从 ChatResponse 的 usage 元数据中提取 token 统计信息,
+ * 支持按会话累计和费用估算。
+ */
+public class TokenTracker {
+
+ private final AtomicLong totalInputTokens = new AtomicLong(0);
+ private final AtomicLong totalOutputTokens = new AtomicLong(0);
+ private final AtomicLong totalCacheReadTokens = new AtomicLong(0);
+ private final AtomicLong totalCacheCreationTokens = new AtomicLong(0);
+ private final AtomicLong apiCallCount = new AtomicLong(0);
+
+ /** 模型定价(每百万 token 的美元价格) */
+ private double inputPricePerMillion = 3.0; // Claude Sonnet 4 input
+ private double outputPricePerMillion = 15.0; // Claude Sonnet 4 output
+ private double cacheReadPricePerMillion = 0.3; // 缓存读取
+ private String modelName = "claude-sonnet-4-20250514";
+
+ /** 记录一次 API 调用的 token 使用 */
+ public void recordUsage(long inputTokens, long outputTokens) {
+ totalInputTokens.addAndGet(inputTokens);
+ totalOutputTokens.addAndGet(outputTokens);
+ apiCallCount.incrementAndGet();
+ }
+
+ /** 记录一次包含缓存的 API 调用 */
+ public void recordUsage(long inputTokens, long outputTokens, long cacheRead, long cacheCreation) {
+ totalInputTokens.addAndGet(inputTokens);
+ totalOutputTokens.addAndGet(outputTokens);
+ totalCacheReadTokens.addAndGet(cacheRead);
+ totalCacheCreationTokens.addAndGet(cacheCreation);
+ apiCallCount.incrementAndGet();
+ }
+
+ /** 设置模型和对应定价 */
+ public void setModel(String model) {
+ this.modelName = model;
+ // 根据模型设置定价
+ if (model.contains("opus")) {
+ inputPricePerMillion = 15.0;
+ outputPricePerMillion = 75.0;
+ cacheReadPricePerMillion = 1.5;
+ } else if (model.contains("sonnet")) {
+ inputPricePerMillion = 3.0;
+ outputPricePerMillion = 15.0;
+ cacheReadPricePerMillion = 0.3;
+ } else if (model.contains("haiku")) {
+ inputPricePerMillion = 0.25;
+ outputPricePerMillion = 1.25;
+ cacheReadPricePerMillion = 0.03;
+ }
+ }
+
+ public long getInputTokens() { return totalInputTokens.get(); }
+ public long getOutputTokens() { return totalOutputTokens.get(); }
+ public long getCacheReadTokens() { return totalCacheReadTokens.get(); }
+ public long getCacheCreationTokens() { return totalCacheCreationTokens.get(); }
+ public long getTotalTokens() { return totalInputTokens.get() + totalOutputTokens.get(); }
+ public long getApiCallCount() { return apiCallCount.get(); }
+ public String getModelName() { return modelName; }
+
+ /** 估算当前会话费用(美元) */
+ public double estimateCost() {
+ double inputCost = totalInputTokens.get() * inputPricePerMillion / 1_000_000.0;
+ double outputCost = totalOutputTokens.get() * outputPricePerMillion / 1_000_000.0;
+ double cacheCost = totalCacheReadTokens.get() * cacheReadPricePerMillion / 1_000_000.0;
+ return inputCost + outputCost + cacheCost;
+ }
+
+ /** 重置统计 */
+ public void reset() {
+ totalInputTokens.set(0);
+ totalOutputTokens.set(0);
+ totalCacheReadTokens.set(0);
+ totalCacheCreationTokens.set(0);
+ apiCallCount.set(0);
+ }
+
+ /** 格式化 token 数量(带千位分隔) */
+ public static String formatTokens(long tokens) {
+ if (tokens < 1000) return String.valueOf(tokens);
+ if (tokens < 1_000_000) return String.format("%.1fK", tokens / 1000.0);
+ return String.format("%.2fM", tokens / 1_000_000.0);
+ }
+}
+ *
+ *
+ * ---
+ * name: verify-tests
+ * description: Run all tests after changes
+ * whenToUse: After modifying code
+ * ---
+ * [技能内容 markdown]
+ *
+ */
+public class SkillLoader {
+
+ private static final Logger log = LoggerFactory.getLogger(SkillLoader.class);
+
+ private final Path projectDir;
+ private final List