refactor: extract utility classes and split AppConfig

- CommandUtils: shared formatting (header, separator, formatBytes, formatDuration, progressBar, truncate)
- ToolValidator: input validation (requireString, getString, getInt, getBoolean, validatePathInWorkDir)
- ProcessExecutor: consolidated ProcessBuilder with timeout, cleanup, shell execution
- AbstractReadOnlyTool: base class for read-only tools
- Split AppConfig -> ToolConfiguration + CommandConfiguration + AppConfig (core)
- Applied CommandUtils to 16 command files, removed duplicated private methods
- Applied ToolValidator + ProcessExecutor to NotificationTool

Net: -403 lines removed, 87 tests still passing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pull/1/head
abel533 1 month ago
parent 5929f50b3a
commit 80f43480c1
  1. 159
      src/main/java/com/claudecode/command/CommandUtils.java
  2. 4
      src/main/java/com/claudecode/command/impl/ContextVizCommand.java
  3. 7
      src/main/java/com/claudecode/command/impl/CostCommand.java
  4. 6
      src/main/java/com/claudecode/command/impl/DebugCommand.java
  5. 40
      src/main/java/com/claudecode/command/impl/EnvCommand.java
  6. 56
      src/main/java/com/claudecode/command/impl/HeapdumpCommand.java
  7. 27
      src/main/java/com/claudecode/command/impl/McpCommand.java
  8. 51
      src/main/java/com/claudecode/command/impl/PerformanceCommand.java
  9. 11
      src/main/java/com/claudecode/command/impl/PluginCommand.java
  10. 12
      src/main/java/com/claudecode/command/impl/ReleaseNotesCommand.java
  11. 4
      src/main/java/com/claudecode/command/impl/ResetLimitsCommand.java
  12. 6
      src/main/java/com/claudecode/command/impl/SandboxCommand.java
  13. 13
      src/main/java/com/claudecode/command/impl/SkillsCommand.java
  14. 16
      src/main/java/com/claudecode/command/impl/StatusCommand.java
  15. 20
      src/main/java/com/claudecode/command/impl/TasksCommand.java
  16. 6
      src/main/java/com/claudecode/command/impl/TraceCommand.java
  17. 5
      src/main/java/com/claudecode/command/impl/VersionCommand.java
  18. 149
      src/main/java/com/claudecode/config/AppConfig.java
  19. 89
      src/main/java/com/claudecode/config/CommandConfiguration.java
  20. 80
      src/main/java/com/claudecode/config/ToolConfiguration.java
  21. 53
      src/main/java/com/claudecode/tool/AbstractReadOnlyTool.java
  22. 90
      src/main/java/com/claudecode/tool/ToolValidator.java
  23. 81
      src/main/java/com/claudecode/tool/impl/NotificationTool.java
  24. 115
      src/main/java/com/claudecode/tool/util/ProcessExecutor.java

@ -0,0 +1,159 @@
package com.claudecode.command;
import com.claudecode.console.AnsiStyle;
/**
* 命令输出格式化工具 消除命令实现中的重复格式化代码
* <p>
* 提供标准化的命令输出格式标题分隔线成功/失败消息
* 字节/时间格式化进度条等
*/
public final class CommandUtils {
private CommandUtils() {}
// ==================== 标题和分隔线 ====================
/**
* 格式化命令标题行
* 输出: "\n ⚡ Title\n ──────────\n\n"
*/
public static String header(String emoji, String title) {
return "\n" + AnsiStyle.bold(" " + emoji + " " + title + "\n")
+ separator(40) + "\n\n";
}
/**
* 格式化命令标题行 emoji
*/
public static String header(String title) {
return "\n" + AnsiStyle.bold(" " + title + "\n")
+ separator(40) + "\n\n";
}
/**
* 生成分隔线
*/
public static String separator(int width) {
return " " + "─".repeat(width);
}
// ==================== 状态消息 ====================
/**
* 成功消息绿色
*/
public static String success(String message) {
return " " + AnsiStyle.green("✓") + " " + message;
}
/**
* 错误消息红色
*/
public static String error(String message) {
return " " + AnsiStyle.red("✗") + " " + message;
}
/**
* 警告消息黄色
*/
public static String warning(String message) {
return " " + AnsiStyle.yellow("⚠") + " " + message;
}
/**
* 信息消息蓝色
*/
public static String info(String message) {
return " " + AnsiStyle.cyan("ℹ") + " " + message;
}
/**
* 子标题粗体缩进
*/
public static String subtitle(String title) {
return AnsiStyle.bold(" " + title);
}
/**
* 键值对行
*/
public static String keyValue(String key, Object value) {
return String.format(" %-12s %s", key + ":", value);
}
/**
* 键值对行自定义宽度
*/
public static String keyValue(String key, Object value, int keyWidth) {
return String.format(" %-" + keyWidth + "s %s", key + ":", value);
}
// ==================== 格式化工具 ====================
/**
* 格式化字节数为人类可读形式
*/
public static String formatBytes(long bytes) {
if (bytes < 0) return "N/A";
if (bytes >= 1_073_741_824) return String.format("%.1fGB", bytes / 1_073_741_824.0);
if (bytes >= 1_048_576) return String.format("%.1fMB", bytes / 1_048_576.0);
if (bytes >= 1_024) return String.format("%.0fKB", bytes / 1_024.0);
return bytes + "B";
}
/**
* 格式化秒数为人类可读时长
*/
public static String formatDuration(long seconds) {
if (seconds < 0) return "N/A";
if (seconds < 60) return seconds + "s";
if (seconds < 3600) return (seconds / 60) + "m " + (seconds % 60) + "s";
return (seconds / 3600) + "h " + ((seconds % 3600) / 60) + "m";
}
/**
* 格式化毫秒为人类可读时长
*/
public static String formatMillis(long ms) {
if (ms < 1000) return ms + "ms";
return formatDuration(ms / 1000);
}
/**
* 截断字符串到最大长度
*/
public static String truncate(String text, int maxLen) {
if (text == null) return "";
if (text.length() <= maxLen) return text;
return text.substring(0, maxLen - 3) + "...";
}
// ==================== 进度条 ====================
/**
* 生成带颜色的进度条
*/
public static String progressBar(double ratio, int width) {
ratio = Math.max(0, Math.min(1.0, ratio));
int filled = (int) (ratio * width);
String bar = "█".repeat(filled);
String empty = "░".repeat(width - filled);
String colored;
if (ratio > 0.8) colored = AnsiStyle.red(bar);
else if (ratio > 0.5) colored = AnsiStyle.yellow(bar);
else colored = AnsiStyle.green(bar);
return "[" + colored + empty + "] " + String.format("%.0f%%", ratio * 100);
}
// ==================== 参数解析 ====================
/**
* 安全解析命令参数去空null空字符串
*/
public static String parseArgs(String args) {
return (args == null) ? "" : args.strip();
}
}

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import com.claudecode.core.TokenEstimationService; import com.claudecode.core.TokenEstimationService;
@ -24,8 +25,7 @@ public class ContextVizCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n").append(AnsiStyle.bold(" 📊 Context Window Visualization\n")); sb.append(CommandUtils.header("📊", "Context Window Visualization"));
sb.append(" ").append("─".repeat(45)).append("\n\n");
if (context.agentLoop() == null) { if (context.agentLoop() == null) {
return sb.append(" No active agent loop\n").toString(); return sb.append(" No active agent loop\n").toString();

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import com.claudecode.core.TokenTracker; import com.claudecode.core.TokenTracker;
@ -29,9 +30,7 @@ public class CostCommand implements SlashCommand {
int msgCount = context.agentLoop().getMessageHistory().size(); int msgCount = context.agentLoop().getMessageHistory().size();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append(CommandUtils.header("💰", "Token Usage & Cost"));
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("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("API Calls: ")).append(tracker.getApiCallCount()).append("\n");
@ -47,7 +46,7 @@ public class CostCommand implements SlashCommand {
sb.append(" ").append(AnsiStyle.bold("Cache create: ")).append(formatTokenLine(tracker.getCacheCreationTokens())).append("\n"); sb.append(" ").append(AnsiStyle.bold("Cache create: ")).append(formatTokenLine(tracker.getCacheCreationTokens())).append("\n");
} }
sb.append(" ").append("─".repeat(30)).append("\n"); sb.append(CommandUtils.separator(30)).append("\n");
sb.append(" ").append(AnsiStyle.bold("Total: ")).append(TokenTracker.formatTokens(tracker.getTotalTokens())).append(" tokens\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"); sb.append(" ").append(AnsiStyle.bold("Est. Cost: ")).append(AnsiStyle.green("$" + String.format("%.4f", tracker.estimateCost()))).append("\n");

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import com.claudecode.core.InternalLogger; import com.claudecode.core.InternalLogger;
@ -23,11 +24,10 @@ public class DebugCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
String trimmed = (args == null) ? "" : args.trim(); String trimmed = CommandUtils.parseArgs(args);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n").append(AnsiStyle.bold(" 🐛 Debug Mode\n")); sb.append(CommandUtils.header("🐛", "Debug Mode"));
sb.append(" ").append("─".repeat(40)).append("\n\n");
if (context.agentLoop() == null) { if (context.agentLoop() == null) {
return sb.append(" No active agent loop\n").toString(); return sb.append(" No active agent loop\n").toString();

@ -1,11 +1,11 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import java.io.File; import java.io.File;
import java.lang.management.ManagementFactory;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
@ -23,32 +23,28 @@ public class EnvCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
String trimmed = (args == null) ? "" : args.trim(); String trimmed = CommandUtils.parseArgs(args);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n").append(AnsiStyle.bold(" 🔧 Environment\n")); sb.append(CommandUtils.header("🔧", "Environment"));
sb.append(" ").append("─".repeat(50)).append("\n\n");
// System info sb.append(CommandUtils.subtitle("System")).append("\n");
sb.append(AnsiStyle.bold(" System\n"));
sb.append(" OS: ").append(System.getProperty("os.name")).append(" ") sb.append(" OS: ").append(System.getProperty("os.name")).append(" ")
.append(System.getProperty("os.version")).append("\n"); .append(System.getProperty("os.version")).append("\n");
sb.append(" Java: ").append(System.getProperty("java.version")) sb.append(" Java: ").append(System.getProperty("java.version"))
.append(" (").append(System.getProperty("java.vendor")).append(")\n"); .append(" (").append(System.getProperty("java.vendor")).append(")\n");
sb.append(" JVM: ").append(System.getProperty("java.vm.name")).append("\n"); sb.append(" JVM: ").append(System.getProperty("java.vm.name")).append("\n");
sb.append(" Heap: ").append(formatBytes(Runtime.getRuntime().totalMemory())) sb.append(" Heap: ").append(CommandUtils.formatBytes(Runtime.getRuntime().totalMemory()))
.append(" / ").append(formatBytes(Runtime.getRuntime().maxMemory())).append("\n"); .append(" / ").append(CommandUtils.formatBytes(Runtime.getRuntime().maxMemory())).append("\n");
sb.append(" PID: ").append(ProcessHandle.current().pid()).append("\n\n"); sb.append(" PID: ").append(ProcessHandle.current().pid()).append("\n\n");
// Work directory sb.append(CommandUtils.subtitle("Paths")).append("\n");
sb.append(AnsiStyle.bold(" Paths\n"));
sb.append(" WorkDir: ").append(System.getProperty("user.dir")).append("\n"); sb.append(" WorkDir: ").append(System.getProperty("user.dir")).append("\n");
sb.append(" Home: ").append(System.getProperty("user.home")).append("\n"); sb.append(" Home: ").append(System.getProperty("user.home")).append("\n");
sb.append(" Config: ").append(System.getProperty("user.home")) sb.append(" Config: ").append(System.getProperty("user.home"))
.append(File.separator).append(".claude-code-java").append("\n\n"); .append(File.separator).append(".claude-code-java").append("\n\n");
// Relevant env vars sb.append(CommandUtils.subtitle("Environment Variables")).append("\n");
sb.append(AnsiStyle.bold(" Environment Variables\n"));
List<String> relevantVars = List.of( List<String> relevantVars = List.of(
"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CLAUDE_CODE_", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CLAUDE_CODE_",
"JAVA_HOME", "PATH", "SHELL", "TERM", "EDITOR" "JAVA_HOME", "PATH", "SHELL", "TERM", "EDITOR"
@ -57,24 +53,14 @@ public class EnvCommand implements SlashCommand {
Map<String, String> env = new TreeMap<>(System.getenv()); Map<String, String> env = new TreeMap<>(System.getenv());
for (Map.Entry<String, String> entry : env.entrySet()) { for (Map.Entry<String, String> entry : env.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
boolean show = false; boolean show = relevantVars.stream().anyMatch(p -> key.startsWith(p) || key.equals(p));
for (String prefix : relevantVars) {
if (key.startsWith(prefix) || key.equals(prefix)) {
show = true;
break;
}
}
if (!show && !trimmed.equals("all")) continue; if (!show && !trimmed.equals("all")) continue;
String value = entry.getValue(); String value = entry.getValue();
// Mask secrets
if (key.contains("KEY") || key.contains("SECRET") || key.contains("TOKEN")) { if (key.contains("KEY") || key.contains("SECRET") || key.contains("TOKEN")) {
value = value.length() > 8 ? value.substring(0, 4) + "****" + value.substring(value.length() - 4) : "****"; value = value.length() > 8 ? value.substring(0, 4) + "****" + value.substring(value.length() - 4) : "****";
} }
// Truncate long values value = CommandUtils.truncate(value, 80);
if (value.length() > 80) {
value = value.substring(0, 77) + "...";
}
sb.append(" ").append(AnsiStyle.cyan(key)).append("=").append(value).append("\n"); sb.append(" ").append(AnsiStyle.cyan(key)).append("=").append(value).append("\n");
} }
@ -84,10 +70,4 @@ public class EnvCommand implements SlashCommand {
return sb.toString(); return sb.toString();
} }
private String formatBytes(long bytes) {
if (bytes >= 1_073_741_824) return String.format("%.1fGB", bytes / 1_073_741_824.0);
if (bytes >= 1_048_576) return String.format("%.0fMB", bytes / 1_048_576.0);
return String.format("%.0fKB", bytes / 1_024.0);
}
} }

@ -1,10 +1,10 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import java.io.File;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean; import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryPoolMXBean; import java.lang.management.MemoryPoolMXBean;
@ -12,11 +12,9 @@ import java.lang.management.MemoryUsage;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List;
/** /**
* /heapdump 命令 JVM 堆转储Java 独有优势 * /heapdump 命令 JVM 堆转储Java 独有优势
* 使用 HotSpotDiagnosticMXBean 进行堆转储
*/ */
public class HeapdumpCommand implements SlashCommand { public class HeapdumpCommand implements SlashCommand {
@ -28,45 +26,39 @@ public class HeapdumpCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
String trimmed = (args == null) ? "" : args.trim(); String trimmed = CommandUtils.parseArgs(args);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n").append(AnsiStyle.bold(" 📦 JVM Heap Dump\n")); sb.append(CommandUtils.header("📦", "JVM Heap Dump"));
sb.append(" ").append("─".repeat(40)).append("\n\n");
if (trimmed.equals("info") || trimmed.isEmpty()) { if (trimmed.equals("info") || trimmed.isEmpty()) {
// Show memory pool details
MemoryMXBean memBean = ManagementFactory.getMemoryMXBean(); MemoryMXBean memBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heap = memBean.getHeapMemoryUsage(); MemoryUsage heap = memBean.getHeapMemoryUsage();
MemoryUsage nonHeap = memBean.getNonHeapMemoryUsage(); MemoryUsage nonHeap = memBean.getNonHeapMemoryUsage();
sb.append(AnsiStyle.bold(" Heap Memory\n")); sb.append(CommandUtils.subtitle("Heap Memory")).append("\n");
sb.append(" Used: ").append(formatBytes(heap.getUsed())).append("\n"); sb.append(" Used: ").append(CommandUtils.formatBytes(heap.getUsed())).append("\n");
sb.append(" Committed: ").append(formatBytes(heap.getCommitted())).append("\n"); sb.append(" Committed: ").append(CommandUtils.formatBytes(heap.getCommitted())).append("\n");
sb.append(" Max: ").append(formatBytes(heap.getMax())).append("\n\n"); sb.append(" Max: ").append(CommandUtils.formatBytes(heap.getMax())).append("\n\n");
sb.append(AnsiStyle.bold(" Non-Heap Memory\n")); sb.append(CommandUtils.subtitle("Non-Heap Memory")).append("\n");
sb.append(" Used: ").append(formatBytes(nonHeap.getUsed())).append("\n"); sb.append(" Used: ").append(CommandUtils.formatBytes(nonHeap.getUsed())).append("\n");
sb.append(" Committed: ").append(formatBytes(nonHeap.getCommitted())).append("\n\n"); sb.append(" Committed: ").append(CommandUtils.formatBytes(nonHeap.getCommitted())).append("\n\n");
sb.append(AnsiStyle.bold(" Memory Pools\n")); sb.append(CommandUtils.subtitle("Memory Pools")).append("\n");
for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) { for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
MemoryUsage usage = pool.getUsage(); MemoryUsage usage = pool.getUsage();
if (usage != null && usage.getUsed() > 0) { if (usage != null && usage.getUsed() > 0) {
sb.append(" ").append(String.format("%-25s", pool.getName())) sb.append(" ").append(String.format("%-25s", pool.getName()))
.append(formatBytes(usage.getUsed())).append("\n"); .append(CommandUtils.formatBytes(usage.getUsed())).append("\n");
} }
} }
sb.append("\n").append(AnsiStyle.dim(" Run /heapdump dump to generate a heap dump file")); sb.append("\n").append(AnsiStyle.dim(" Run /heapdump dump to generate a heap dump file"));
} else if (trimmed.startsWith("dump")) { } else if (trimmed.startsWith("dump")) {
// Determine output path
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")); String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
String filename = trimmed.length() > 5 ? trimmed.substring(5).trim() : ""; String filename = trimmed.length() > 5 ? trimmed.substring(5).trim() : "";
if (filename.isEmpty()) { if (filename.isEmpty()) filename = "heapdump-" + timestamp + ".hprof";
filename = "heapdump-" + timestamp + ".hprof";
}
Path dumpPath = Path.of(System.getProperty("user.dir"), filename); Path dumpPath = Path.of(System.getProperty("user.dir"), filename);
try { try {
@ -74,29 +66,28 @@ public class HeapdumpCommand implements SlashCommand {
com.sun.management.HotSpotDiagnosticMXBean.class); com.sun.management.HotSpotDiagnosticMXBean.class);
hotspot.dumpHeap(dumpPath.toString(), true); hotspot.dumpHeap(dumpPath.toString(), true);
long fileSize = dumpPath.toFile().length(); long fileSize = dumpPath.toFile().length();
sb.append("Heap dump saved to:\n"); sb.append(CommandUtils.success("Heap dump saved to:")).append("\n");
sb.append(" ").append(AnsiStyle.cyan(dumpPath.toString())).append("\n"); sb.append(" ").append(AnsiStyle.cyan(dumpPath.toString())).append("\n");
sb.append(" Size: ").append(formatBytes(fileSize)).append("\n\n"); sb.append(" Size: ").append(CommandUtils.formatBytes(fileSize)).append("\n\n");
sb.append(AnsiStyle.dim(" Analyze with: jhat, MAT, or VisualVM")); sb.append(AnsiStyle.dim(" Analyze with: jhat, MAT, or VisualVM"));
} catch (Exception e) { } catch (Exception e) {
sb.append(" ❌ Failed to create heap dump: ").append(e.getMessage()).append("\n"); sb.append(CommandUtils.error("Failed to create heap dump: " + e.getMessage())).append("\n");
sb.append(AnsiStyle.dim(" Requires HotSpot JVM (OpenJDK or Oracle JDK)")); sb.append(AnsiStyle.dim(" Requires HotSpot JVM (OpenJDK or Oracle JDK)"));
} }
} else if (trimmed.equals("gc")) { } else if (trimmed.equals("gc")) {
// Trigger GC and report
long beforeUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); long beforeUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.gc(); System.gc();
long afterUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); long afterUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
long freed = beforeUsed - afterUsed; long freed = beforeUsed - afterUsed;
sb.append(" 🗑 Garbage collection triggered\n"); sb.append(" 🗑 Garbage collection triggered\n");
sb.append(" Before: ").append(formatBytes(beforeUsed)).append("\n"); sb.append(" Before: ").append(CommandUtils.formatBytes(beforeUsed)).append("\n");
sb.append(" After: ").append(formatBytes(afterUsed)).append("\n"); sb.append(" After: ").append(CommandUtils.formatBytes(afterUsed)).append("\n");
sb.append(" Freed: ").append(AnsiStyle.green(formatBytes(Math.max(0, freed)))).append("\n"); sb.append(" Freed: ").append(AnsiStyle.green(CommandUtils.formatBytes(Math.max(0, freed)))).append("\n");
} else { } else {
sb.append(AnsiStyle.bold(" Subcommands\n")); sb.append(CommandUtils.subtitle("Subcommands")).append("\n");
sb.append(" /heapdump Show memory pool info\n"); sb.append(" /heapdump Show memory pool info\n");
sb.append(" /heapdump dump Generate .hprof file\n"); sb.append(" /heapdump dump Generate .hprof file\n");
sb.append(" /heapdump gc Trigger garbage collection\n"); sb.append(" /heapdump gc Trigger garbage collection\n");
@ -104,11 +95,4 @@ public class HeapdumpCommand implements SlashCommand {
return sb.toString(); return sb.toString();
} }
private String formatBytes(long bytes) {
if (bytes < 0) return "N/A";
if (bytes >= 1_073_741_824) return String.format("%.1fGB", bytes / 1_073_741_824.0);
if (bytes >= 1_048_576) return String.format("%.1fMB", bytes / 1_048_576.0);
return String.format("%.0fKB", bytes / 1_024.0);
}
} }

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import com.claudecode.mcp.McpClient; import com.claudecode.mcp.McpClient;
@ -47,7 +48,7 @@ public class McpCommand implements SlashCommand {
return AnsiStyle.red(" ❌ MCP manager not initialized"); return AnsiStyle.red(" ❌ MCP manager not initialized");
} }
String trimmed = args.strip(); String trimmed = CommandUtils.parseArgs(args);
if (trimmed.isEmpty()) { if (trimmed.isEmpty()) {
return showStatus(manager); return showStatus(manager);
} }
@ -72,9 +73,7 @@ public class McpCommand implements SlashCommand {
*/ */
private String showStatus(McpManager manager) { private String showStatus(McpManager manager) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append(CommandUtils.header("🔌", "MCP Server Status"));
sb.append(AnsiStyle.bold(" 🔌 MCP Server Status\n"));
sb.append(" ").append("─".repeat(50)).append("\n\n");
Map<String, McpClient> clients = manager.getClients(); Map<String, McpClient> clients = manager.getClients();
if (clients.isEmpty()) { if (clients.isEmpty()) {
@ -159,7 +158,7 @@ public class McpCommand implements SlashCommand {
for (McpClient.McpTool tool : client.getTools()) { for (McpClient.McpTool tool : client.getTools()) {
sb.append(" • ").append(tool.name()); sb.append(" • ").append(tool.name());
if (!tool.description().isEmpty()) { if (!tool.description().isEmpty()) {
sb.append(AnsiStyle.dim(" - " + truncate(tool.description(), 60))); sb.append(AnsiStyle.dim(" - " + CommandUtils.truncate(tool.description(), 60)));
} }
sb.append("\n"); sb.append("\n");
} }
@ -193,9 +192,7 @@ public class McpCommand implements SlashCommand {
*/ */
private String handleTools(McpManager manager, String args) { private String handleTools(McpManager manager, String args) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append(CommandUtils.header("🛠", "MCP Tools"));
sb.append(AnsiStyle.bold(" 🛠 MCP Tools\n"));
sb.append(" ").append("─".repeat(50)).append("\n\n");
String serverFilter = args.isEmpty() ? null : args.split("\\s+")[0]; String serverFilter = args.isEmpty() ? null : args.split("\\s+")[0];
@ -220,7 +217,7 @@ public class McpCommand implements SlashCommand {
} }
if (tool.inputSchema() != null) { if (tool.inputSchema() != null) {
sb.append(" ").append(AnsiStyle.dim("Schema: " + sb.append(" ").append(AnsiStyle.dim("Schema: " +
truncate(tool.inputSchema().toString(), 80))).append("\n"); CommandUtils.truncate(tool.inputSchema().toString(), 80))).append("\n");
} }
sb.append("\n"); sb.append("\n");
} }
@ -234,9 +231,7 @@ public class McpCommand implements SlashCommand {
*/ */
private String handleResources(McpManager manager, String args) { private String handleResources(McpManager manager, String args) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append(CommandUtils.header("📦", "MCP Resources"));
sb.append(AnsiStyle.bold(" 📦 MCP Resources\n"));
sb.append(" ").append("─".repeat(50)).append("\n\n");
String serverFilter = args.isEmpty() ? null : args.split("\\s+")[0]; String serverFilter = args.isEmpty() ? null : args.split("\\s+")[0];
@ -325,14 +320,6 @@ public class McpCommand implements SlashCommand {
"""; """;
} }
/**
* 截断字符串
*/
private static String truncate(String s, int maxLen) {
if (s == null) return "";
return s.length() <= maxLen ? s : s.substring(0, maxLen - 3) + "...";
}
// ========== McpManager 持有者(简单单例,供命令和其他组件访问) ========== // ========== McpManager 持有者(简单单例,供命令和其他组件访问) ==========
/** /**

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import com.claudecode.telemetry.MetricsCollector; import com.claudecode.telemetry.MetricsCollector;
@ -24,45 +25,40 @@ public class PerformanceCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n").append(AnsiStyle.bold(" ⚡ Performance Statistics\n")); sb.append(CommandUtils.header("⚡", "Performance Statistics"));
sb.append(" ").append("─".repeat(40)).append("\n\n");
// JVM stats
Runtime runtime = Runtime.getRuntime(); Runtime runtime = Runtime.getRuntime();
long totalMem = runtime.totalMemory(); long totalMem = runtime.totalMemory();
long freeMem = runtime.freeMemory(); long freeMem = runtime.freeMemory();
long usedMem = totalMem - freeMem; long usedMem = totalMem - freeMem;
long maxMem = runtime.maxMemory(); long maxMem = runtime.maxMemory();
sb.append(AnsiStyle.bold(" Memory\n")); sb.append(CommandUtils.subtitle("Memory")).append("\n");
sb.append(" Used: ").append(formatBytes(usedMem)).append("\n"); sb.append(" Used: ").append(CommandUtils.formatBytes(usedMem)).append("\n");
sb.append(" Allocated: ").append(formatBytes(totalMem)).append("\n"); sb.append(" Allocated: ").append(CommandUtils.formatBytes(totalMem)).append("\n");
sb.append(" Max: ").append(formatBytes(maxMem)).append("\n"); sb.append(" Max: ").append(CommandUtils.formatBytes(maxMem)).append("\n");
sb.append(" Usage: ").append(memBar(usedMem, maxMem)).append("\n\n"); sb.append(" Usage: ").append(CommandUtils.progressBar((double) usedMem / maxMem, 20)).append("\n\n");
// Thread stats
int threadCount = Thread.activeCount(); int threadCount = Thread.activeCount();
sb.append(AnsiStyle.bold(" Threads\n")); sb.append(CommandUtils.subtitle("Threads")).append("\n");
sb.append(" Active: ").append(threadCount).append("\n"); sb.append(" Active: ").append(threadCount).append("\n");
sb.append(" Available: ").append(runtime.availableProcessors()).append(" CPUs\n\n"); sb.append(" Available: ").append(runtime.availableProcessors()).append(" CPUs\n\n");
// GC stats
long gcCount = 0; long gcCount = 0;
long gcTime = 0; long gcTime = 0;
for (var gc : java.lang.management.ManagementFactory.getGarbageCollectorMXBeans()) { for (var gc : java.lang.management.ManagementFactory.getGarbageCollectorMXBeans()) {
gcCount += gc.getCollectionCount(); gcCount += gc.getCollectionCount();
gcTime += gc.getCollectionTime(); gcTime += gc.getCollectionTime();
} }
sb.append(AnsiStyle.bold(" GC\n")); sb.append(CommandUtils.subtitle("GC")).append("\n");
sb.append(" Collections: ").append(gcCount).append("\n"); sb.append(" Collections: ").append(gcCount).append("\n");
sb.append(" Total time: ").append(gcTime).append("ms\n\n"); sb.append(" Total time: ").append(CommandUtils.formatMillis(gcTime)).append("\n\n");
// Metrics if available
if (context.agentLoop() != null) { if (context.agentLoop() != null) {
Object metricsObj = context.agentLoop().getToolContext().get("METRICS_COLLECTOR"); Object metricsObj = context.agentLoop().getToolContext().get("METRICS_COLLECTOR");
if (metricsObj instanceof MetricsCollector metrics) { if (metricsObj instanceof MetricsCollector metrics) {
sb.append(AnsiStyle.bold(" Session Metrics\n")); sb.append(CommandUtils.subtitle("Session Metrics")).append("\n");
sb.append(" Duration: ").append(formatDuration(metrics.getSessionDurationSeconds())).append("\n"); sb.append(" Duration: ").append(CommandUtils.formatDuration(metrics.getSessionDurationSeconds())).append("\n");
var toolUsage = metrics.getToolUsage(); var toolUsage = metrics.getToolUsage();
if (!toolUsage.isEmpty()) { if (!toolUsage.isEmpty()) {
sb.append(" Tool calls: ").append(toolUsage.values().stream().mapToLong(Long::longValue).sum()).append("\n"); sb.append(" Tool calls: ").append(toolUsage.values().stream().mapToLong(Long::longValue).sum()).append("\n");
@ -78,27 +74,4 @@ public class PerformanceCommand implements SlashCommand {
return sb.toString(); return sb.toString();
} }
private String memBar(long used, long max) {
int barWidth = 20;
double ratio = (double) used / max;
int filled = (int) (ratio * barWidth);
String color = ratio > 0.8 ? AnsiStyle.red("█".repeat(filled))
: ratio > 0.5 ? AnsiStyle.yellow("█".repeat(filled))
: AnsiStyle.green("█".repeat(filled));
return "[" + color + "░".repeat(barWidth - filled) + "] " +
String.format("%.0f%%", ratio * 100);
}
private String formatBytes(long bytes) {
if (bytes >= 1_073_741_824) return String.format("%.1fGB", bytes / 1_073_741_824.0);
if (bytes >= 1_048_576) return String.format("%.0fMB", bytes / 1_048_576.0);
return String.format("%.0fKB", bytes / 1_024.0);
}
private String formatDuration(long seconds) {
if (seconds < 60) return seconds + "s";
if (seconds < 3600) return (seconds / 60) + "m " + (seconds % 60) + "s";
return (seconds / 3600) + "h " + ((seconds % 3600) / 60) + "m";
}
} }

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import com.claudecode.plugin.*; import com.claudecode.plugin.*;
@ -55,7 +56,7 @@ public class PluginCommand implements SlashCommand {
return AnsiStyle.red(" ✗ Plugin system not initialized"); return AnsiStyle.red(" ✗ Plugin system not initialized");
} }
String trimmed = (args == null) ? "" : args.trim(); String trimmed = CommandUtils.parseArgs(args);
// 无参数:列出所有插件 // 无参数:列出所有插件
if (trimmed.isEmpty()) { if (trimmed.isEmpty()) {
@ -87,9 +88,7 @@ public class PluginCommand implements SlashCommand {
private String listPlugins(PluginManager manager) { private String listPlugins(PluginManager manager) {
List<PluginInfo> plugins = manager.getPlugins(); List<PluginInfo> plugins = manager.getPlugins();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append(CommandUtils.header("🔌", "Loaded Plugins"));
sb.append(AnsiStyle.bold(" 🔌 Loaded Plugins")).append("\n");
sb.append(" ").append("─".repeat(50)).append("\n\n");
if (plugins.isEmpty()) { if (plugins.isEmpty()) {
sb.append(AnsiStyle.dim(" No plugins loaded.")).append("\n"); sb.append(AnsiStyle.dim(" No plugins loaded.")).append("\n");
@ -174,9 +173,7 @@ public class PluginCommand implements SlashCommand {
Plugin p = info.plugin(); Plugin p = info.plugin();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append(CommandUtils.header("🔌", "Plugin Details"));
sb.append(AnsiStyle.bold(" 🔌 Plugin Details")).append("\n");
sb.append(" ").append("─".repeat(40)).append("\n\n");
sb.append(" ").append(AnsiStyle.bold("Name: ")).append(p.name()).append("\n"); sb.append(" ").append(AnsiStyle.bold("Name: ")).append(p.name()).append("\n");
sb.append(" ").append(AnsiStyle.bold("ID: ")).append(AnsiStyle.cyan(p.id())).append("\n"); sb.append(" ").append(AnsiStyle.bold("ID: ")).append(AnsiStyle.cyan(p.id())).append("\n");

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
@ -25,11 +26,10 @@ public class ReleaseNotesCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n").append(AnsiStyle.bold(" 📋 Release Notes\n")); sb.append(CommandUtils.header("📋", "Release Notes"));
sb.append(" ").append("─".repeat(50)).append("\n\n");
sb.append(AnsiStyle.bold(" v0.4.0 — Phase 4: Commands, Tools & Services\n")); sb.append(AnsiStyle.bold(" v0.4.0 — Phase 4: Commands, Tools & Services\n"));
sb.append(" ").append("─".repeat(45)).append("\n"); sb.append(CommandUtils.separator(45)).append("\n");
sb.append(" • LSPTool: code navigation via Language Server Protocol\n"); sb.append(" • LSPTool: code navigation via Language Server Protocol\n");
sb.append(" • BriefTool: output verbosity control\n"); sb.append(" • BriefTool: output verbosity control\n");
sb.append(" • NotificationTool: cross-platform desktop notifications\n"); sb.append(" • NotificationTool: cross-platform desktop notifications\n");
@ -38,7 +38,7 @@ public class ReleaseNotesCommand implements SlashCommand {
sb.append(" • Debug commands: /debug, /heapdump, /trace, /ctx-viz\n\n"); sb.append(" • Debug commands: /debug, /heapdump, /trace, /ctx-viz\n\n");
sb.append(AnsiStyle.bold(" v0.3.0 — Phase 3: Advanced Infrastructure\n")); sb.append(AnsiStyle.bold(" v0.3.0 — Phase 3: Advanced Infrastructure\n"));
sb.append(" ").append("─".repeat(45)).append("\n"); sb.append(CommandUtils.separator(45)).append("\n");
sb.append(" • Server Mode: WebSocket direct connect for SDK integration\n"); sb.append(" • Server Mode: WebSocket direct connect for SDK integration\n");
sb.append(" • Git Worktree: parallel branch isolation for agent tasks\n"); sb.append(" • Git Worktree: parallel branch isolation for agent tasks\n");
sb.append(" • LSP Integration: JSON-RPC client, multi-server, diagnostics\n"); sb.append(" • LSP Integration: JSON-RPC client, multi-server, diagnostics\n");
@ -46,7 +46,7 @@ public class ReleaseNotesCommand implements SlashCommand {
sb.append(" • Plugin Marketplace: install, search, auto-update plugins\n\n"); sb.append(" • Plugin Marketplace: install, search, auto-update plugins\n\n");
sb.append(AnsiStyle.bold(" v0.2.0 — Phase 2: Core Features\n")); sb.append(AnsiStyle.bold(" v0.2.0 — Phase 2: Core Features\n"));
sb.append(" ").append("─".repeat(45)).append("\n"); sb.append(CommandUtils.separator(45)).append("\n");
sb.append(" • Plan Mode for multi-step task planning\n"); sb.append(" • Plan Mode for multi-step task planning\n");
sb.append(" • Skills execution system with /skill command\n"); sb.append(" • Skills execution system with /skill command\n");
sb.append(" • Session Memory with CLAUDE.md auto-persist\n"); sb.append(" • Session Memory with CLAUDE.md auto-persist\n");
@ -54,7 +54,7 @@ public class ReleaseNotesCommand implements SlashCommand {
sb.append(" • MCP enhancements: HTTP+SSE, resources, env vars\n\n"); sb.append(" • MCP enhancements: HTTP+SSE, resources, env vars\n\n");
sb.append(AnsiStyle.bold(" v0.1.0 — Phase 1: Foundation\n")); sb.append(AnsiStyle.bold(" v0.1.0 — Phase 1: Foundation\n"));
sb.append(" ").append("─".repeat(45)).append("\n"); sb.append(CommandUtils.separator(45)).append("\n");
sb.append(" • Enhanced system prompts (7 security/style sections)\n"); sb.append(" • Enhanced system prompts (7 security/style sections)\n");
sb.append(" • 8 tool description improvements\n"); sb.append(" • 8 tool description improvements\n");
sb.append(" • New tools: TaskStop, TaskOutput, Sleep, ToolSearch\n"); sb.append(" • New tools: TaskStop, TaskOutput, Sleep, ToolSearch\n");

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import com.claudecode.core.RateLimiter; import com.claudecode.core.RateLimiter;
@ -24,8 +25,7 @@ public class ResetLimitsCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n").append(AnsiStyle.bold(" 🔄 Rate Limit Reset\n")); sb.append(CommandUtils.header("🔄", "Rate Limit Reset"));
sb.append(" ").append("─".repeat(40)).append("\n\n");
if (context.agentLoop() == null) { if (context.agentLoop() == null) {
return sb.append(" No active agent loop\n").toString(); return sb.append(" No active agent loop\n").toString();

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
@ -20,11 +21,10 @@ public class SandboxCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
String trimmed = (args == null) ? "" : args.trim(); String trimmed = CommandUtils.parseArgs(args);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n").append(AnsiStyle.bold(" 🏖 Sandbox Mode\n")); sb.append(CommandUtils.header("🏖", "Sandbox Mode"));
sb.append(" ").append("─".repeat(40)).append("\n\n");
if (context.agentLoop() == null) { if (context.agentLoop() == null) {
return sb.append(" No active agent loop\n").toString(); return sb.append(" No active agent loop\n").toString();

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import com.claudecode.context.SkillLoader; import com.claudecode.context.SkillLoader;
@ -50,9 +51,7 @@ public class SkillsCommand implements SlashCommand {
} }
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append(CommandUtils.header("🎯", "Available Skills"));
sb.append(AnsiStyle.bold(" 🎯 Available Skills\n"));
sb.append(" ").append("─".repeat(50)).append("\n\n");
if (skills.isEmpty()) { if (skills.isEmpty()) {
sb.append(AnsiStyle.dim(" (No available skills)\n\n")); sb.append(AnsiStyle.dim(" (No available skills)\n\n"));
@ -90,9 +89,7 @@ public class SkillsCommand implements SlashCommand {
private String formatSkillDetail(SkillLoader.Skill skill) { private String formatSkillDetail(SkillLoader.Skill skill) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append(CommandUtils.header("🎯", "Skill: " + skill.name()));
sb.append(AnsiStyle.bold(" 🎯 Skill: " + skill.name())).append("\n");
sb.append(" ").append("─".repeat(50)).append("\n\n");
sb.append(" ").append(AnsiStyle.bold("Source: ")).append(skill.source()).append("\n"); sb.append(" ").append(AnsiStyle.bold("Source: ")).append(skill.source()).append("\n");
if (!skill.description().isEmpty()) { if (!skill.description().isEmpty()) {
@ -106,9 +103,7 @@ public class SkillsCommand implements SlashCommand {
// Show content preview // Show content preview
String content = skill.content(); String content = skill.content();
if (content.length() > 500) { content = CommandUtils.truncate(content, 500);
content = content.substring(0, 497) + "...";
}
sb.append(AnsiStyle.dim(" Content:\n")); sb.append(AnsiStyle.dim(" Content:\n"));
for (String line : content.lines().toList()) { for (String line : content.lines().toList()) {
sb.append(AnsiStyle.dim(" │ " + line)).append("\n"); sb.append(AnsiStyle.dim(" │ " + line)).append("\n");

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import com.claudecode.core.TokenTracker; import com.claudecode.core.TokenTracker;
@ -31,9 +32,7 @@ public class StatusCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append(CommandUtils.header("📊", "Session Status"));
sb.append(AnsiStyle.bold(" 📊 Session Status\n"));
sb.append(" ").append("─".repeat(40)).append("\n\n");
// 模型信息 // 模型信息
TokenTracker tracker = context.agentLoop().getTokenTracker(); TokenTracker tracker = context.agentLoop().getTokenTracker();
@ -68,7 +67,7 @@ public class StatusCommand implements SlashCommand {
// 运行时间 // 运行时间
Duration uptime = Duration.between(startTime, Instant.now()); Duration uptime = Duration.between(startTime, Instant.now());
sb.append(" ").append(AnsiStyle.bold("Uptime: ")).append(formatDuration(uptime)).append("\n"); sb.append(" ").append(AnsiStyle.bold("Uptime: ")).append(CommandUtils.formatDuration(uptime.toSeconds())).append("\n");
// Java 版本 // Java 版本
sb.append(" ").append(AnsiStyle.bold("JDK: ")).append(System.getProperty("java.version")).append("\n"); sb.append(" ").append(AnsiStyle.bold("JDK: ")).append(System.getProperty("java.version")).append("\n");
@ -76,12 +75,5 @@ public class StatusCommand implements SlashCommand {
return sb.toString(); 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);
}
} }

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
import com.claudecode.core.TaskManager; import com.claudecode.core.TaskManager;
@ -38,7 +39,7 @@ public class TasksCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
List<TaskInfo> tasks; List<TaskInfo> tasks;
String filter = (args == null) ? "" : args.strip(); String filter = CommandUtils.parseArgs(args);
// Optional status filter // Optional status filter
if (!filter.isEmpty()) { if (!filter.isEmpty()) {
@ -59,7 +60,7 @@ public class TasksCommand implements SlashCommand {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n ").append(AnsiStyle.bold("📋 Tasks")).append(" (").append(tasks.size()).append(")\n"); sb.append("\n ").append(AnsiStyle.bold("📋 Tasks")).append(" (").append(tasks.size()).append(")\n");
sb.append(" ").append("─".repeat(60)).append("\n"); sb.append(CommandUtils.separator(60)).append("\n");
for (TaskInfo task : tasks) { for (TaskInfo task : tasks) {
String icon = switch (task.status()) { String icon = switch (task.status()) {
@ -84,30 +85,21 @@ public class TasksCommand implements SlashCommand {
.append(task.description()).append("\n"); .append(task.description()).append("\n");
// Time info // Time info
String age = formatDuration(Duration.between(task.createdAt(), Instant.now())); String age = CommandUtils.formatDuration(Duration.between(task.createdAt(), Instant.now()).toSeconds());
sb.append(" ").append(AnsiStyle.dim("Created " + age + " ago")); sb.append(" ").append(AnsiStyle.dim("Created " + age + " ago"));
// Result preview for completed/failed // Result preview for completed/failed
if (task.result() != null) { if (task.result() != null) {
String preview = task.result().length() > 60 String preview = CommandUtils.truncate(task.result(), 60);
? task.result().substring(0, 57) + "..."
: task.result();
sb.append(" ").append(AnsiStyle.dim("→ " + preview)); sb.append(" ").append(AnsiStyle.dim("→ " + preview));
} }
sb.append("\n"); sb.append("\n");
} }
// Summary // Summary
sb.append(" ").append("─".repeat(60)).append("\n"); sb.append(CommandUtils.separator(60)).append("\n");
sb.append(" ").append(AnsiStyle.dim(taskManager.getSummary())).append("\n"); sb.append(" ").append(AnsiStyle.dim(taskManager.getSummary())).append("\n");
return sb.toString(); return sb.toString();
} }
private String formatDuration(Duration d) {
if (d.toMinutes() < 1) return d.toSeconds() + "s";
if (d.toHours() < 1) return d.toMinutes() + "m";
if (d.toDays() < 1) return d.toHours() + "h";
return d.toDays() + "d";
}
} }

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
@ -23,11 +24,10 @@ public class TraceCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
String trimmed = (args == null) ? "" : args.trim(); String trimmed = CommandUtils.parseArgs(args);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n").append(AnsiStyle.bold(" 🔍 Request Tracing\n")); sb.append(CommandUtils.header("🔍", "Request Tracing"));
sb.append(" ").append("─".repeat(40)).append("\n\n");
if (context.agentLoop() == null) { if (context.agentLoop() == null) {
return sb.append(" No active agent loop\n").toString(); return sb.append(" No active agent loop\n").toString();

@ -1,6 +1,7 @@
package com.claudecode.command.impl; package com.claudecode.command.impl;
import com.claudecode.command.CommandContext; import com.claudecode.command.CommandContext;
import com.claudecode.command.CommandUtils;
import com.claudecode.command.SlashCommand; import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle; import com.claudecode.console.AnsiStyle;
@ -28,9 +29,7 @@ public class VersionCommand implements SlashCommand {
@Override @Override
public String execute(String args, CommandContext context) { public String execute(String args, CommandContext context) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append(CommandUtils.header("🏷", "Claude Code Java"));
sb.append(AnsiStyle.bold(" 🏷 Claude Code Java\n"));
sb.append(" ").append("─".repeat(40)).append("\n\n");
sb.append(" ").append(AnsiStyle.bold("Version: ")) sb.append(" ").append(AnsiStyle.bold("Version: "))
.append(AnsiStyle.cyan("v" + VERSION)).append("\n"); .append(AnsiStyle.cyan("v" + VERSION)).append("\n");

@ -1,7 +1,6 @@
package com.claudecode.config; package com.claudecode.config;
import com.claudecode.command.CommandRegistry; import com.claudecode.command.CommandRegistry;
import com.claudecode.command.impl.*;
import com.claudecode.context.ClaudeMdLoader; import com.claudecode.context.ClaudeMdLoader;
import com.claudecode.context.GitContext; import com.claudecode.context.GitContext;
import com.claudecode.context.SkillLoader; import com.claudecode.context.SkillLoader;
@ -21,7 +20,8 @@ import com.claudecode.repl.ReplSession;
import com.claudecode.tui.JinkReplSession; import com.claudecode.tui.JinkReplSession;
import com.claudecode.tool.ToolContext; import com.claudecode.tool.ToolContext;
import com.claudecode.tool.ToolRegistry; import com.claudecode.tool.ToolRegistry;
import com.claudecode.tool.impl.*; import com.claudecode.tool.impl.AgentTool;
import com.claudecode.tool.impl.SkillTool;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatModel;
@ -32,6 +32,13 @@ import org.springframework.context.annotation.Configuration;
import java.nio.file.Path; import java.nio.file.Path;
/**
* 核心应用配置 基础设施 Bean 和跨切关注点
* <p>
* 工具注册见 {@link ToolConfiguration}
* 命令注册见 {@link CommandConfiguration}
*/
/** /**
* 应用配置类 Spring Bean 装配 * 应用配置类 Spring Bean 装配
* <p> * <p>
@ -79,144 +86,6 @@ public class AppConfig {
return manager; return manager;
} }
@Bean
public ToolRegistry toolRegistry(TaskManager taskManager, McpManager mcpManager,
ToolContext toolContext, PermissionSettings permissionSettings) {
// 将 TaskManager 和 McpManager 注册到 ToolContext 供工具使用
toolContext.set("TASK_MANAGER", taskManager);
toolContext.set("MCP_MANAGER", mcpManager);
toolContext.set("PERMISSION_SETTINGS", permissionSettings);
ToolRegistry registry = new ToolRegistry();
registry.registerAll(
new BashTool(),
new FileReadTool(),
new FileWriteTool(),
new FileEditTool(),
new GlobTool(),
new GrepTool(),
new ListFilesTool(),
new WebFetchTool(),
new TodoWriteTool(),
new AgentTool(),
new NotebookEditTool(),
new WebSearchTool(),
new AskUserQuestionTool(),
// P2: 任务管理工具
new TaskCreateTool(),
new TaskGetTool(),
new TaskListTool(),
new TaskUpdateTool(),
new TaskStopTool(),
new TaskOutputTool(),
// P2: 配置工具
new ConfigTool(),
// P2: 实用工具
new SleepTool(),
new ToolSearchTool(),
new EnterPlanModeTool(),
new ExitPlanModeTool(),
new SkillTool(),
new SendMessageTool(),
new ListMcpResourcesTool(),
new ReadMcpResourceTool(),
new EnterWorktreeTool(),
new ExitWorktreeTool(),
// P4: 新工具
new LSPTool(),
new BriefTool(),
new NotificationTool()
);
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
for (var client : mcpManager.getClients().values()) {
for (var mcpTool : client.getTools()) {
registry.register(new McpToolBridge(client.getServerName(), mcpTool));
}
}
// 将 ToolRegistry 注入 ToolContext,供 ToolSearchTool 使用
toolContext.set("TOOL_REGISTRY", registry);
return registry;
}
@Bean
public CommandRegistry commandRegistry(PluginManager pluginManager, PermissionSettings permissionSettings,
TaskManager taskManager) {
ConfigCommand configCommand = new ConfigCommand(permissionSettings);
CommandRegistry registry = new CommandRegistry();
registry.registerAll(
// 基础命令
new HelpCommand(),
new ClearCommand(),
new CompactCommand(),
new CostCommand(),
new ModelCommand(),
new StatusCommand(),
new ContextCommand(),
new InitCommand(),
configCommand,
new HistoryCommand(),
// P0 命令
new DiffCommand(),
new VersionCommand(),
new SkillsCommand(),
new MemoryCommand(),
new CopyCommand(),
// P1 命令
new ResumeCommand(),
new ExportCommand(),
new CommitCommand(),
new FilesCommand(),
new PermissionsCommand(permissionSettings),
new TasksCommand(taskManager),
new PlanCommand(permissionSettings),
// P2 命令
new HooksCommand(),
new ReviewCommand(),
new StatsCommand(),
new BranchCommand(),
new RewindCommand(),
new TagCommand(),
new SecurityReviewCommand(),
new McpCommand(),
new PluginCommand(),
// Phase 2F 命令
new DoctorCommand(),
new SessionCommand(),
new AgentCommand(),
new RenameCommand(),
// Phase 4B 命令
new BriefCommand(),
new VimCommand(),
new ThemeCommand(),
new UsageCommand(),
new TipsCommand(),
new OutputStyleCommand(),
new EnvCommand(),
new PerformanceCommand(),
new PrivacyCommand(),
new FeedbackCommand(),
new ReleaseNotesCommand(),
new KeybindingsCommand(),
// Phase 4D 调试命令
new DebugCommand(),
new HeapdumpCommand(),
new TraceCommand(),
new ContextVizCommand(),
new ResetLimitsCommand(),
new SandboxCommand(),
// Exit 放最后
new ExitCommand()
);
// P2: 注册插件提供的命令
pluginManager.registerCommands(registry);
return registry;
}
/** /**
* 根据 claude-code.provider 配置选择 ChatModel * 根据 claude-code.provider 配置选择 ChatModel
*/ */

@ -0,0 +1,89 @@
package com.claudecode.config;
import com.claudecode.command.CommandRegistry;
import com.claudecode.command.impl.*;
import com.claudecode.core.TaskManager;
import com.claudecode.permission.PermissionSettings;
import com.claudecode.plugin.PluginManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 命令注册配置 AppConfig 拆分出来专注于 SlashCommand 注册
*/
@Configuration
public class CommandConfiguration {
@Bean
public CommandRegistry commandRegistry(PluginManager pluginManager, PermissionSettings permissionSettings,
TaskManager taskManager) {
ConfigCommand configCommand = new ConfigCommand(permissionSettings);
CommandRegistry registry = new CommandRegistry();
registry.registerAll(
// 基础命令
new HelpCommand(),
new ClearCommand(),
new CompactCommand(),
new CostCommand(),
new ModelCommand(),
new StatusCommand(),
new ContextCommand(),
new InitCommand(),
configCommand,
new HistoryCommand(),
// 文件/Git 命令
new DiffCommand(),
new VersionCommand(),
new SkillsCommand(),
new MemoryCommand(),
new CopyCommand(),
new ResumeCommand(),
new ExportCommand(),
new CommitCommand(),
new FilesCommand(),
new PermissionsCommand(permissionSettings),
new TasksCommand(taskManager),
new PlanCommand(permissionSettings),
// 协作/审查命令
new HooksCommand(),
new ReviewCommand(),
new StatsCommand(),
new BranchCommand(),
new RewindCommand(),
new TagCommand(),
new SecurityReviewCommand(),
new McpCommand(),
new PluginCommand(),
// 会话/Agent 命令
new DoctorCommand(),
new SessionCommand(),
new AgentCommand(),
new RenameCommand(),
// UX 命令
new BriefCommand(),
new VimCommand(),
new ThemeCommand(),
new UsageCommand(),
new TipsCommand(),
new OutputStyleCommand(),
new EnvCommand(),
new PerformanceCommand(),
new PrivacyCommand(),
new FeedbackCommand(),
new ReleaseNotesCommand(),
new KeybindingsCommand(),
// 调试命令
new DebugCommand(),
new HeapdumpCommand(),
new TraceCommand(),
new ContextVizCommand(),
new ResetLimitsCommand(),
new SandboxCommand(),
// Exit
new ExitCommand()
);
pluginManager.registerCommands(registry);
return registry;
}
}

@ -0,0 +1,80 @@
package com.claudecode.config;
import com.claudecode.mcp.McpManager;
import com.claudecode.tool.ToolContext;
import com.claudecode.tool.ToolRegistry;
import com.claudecode.tool.impl.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.claudecode.core.TaskManager;
import com.claudecode.permission.PermissionSettings;
/**
* 工具注册配置 AppConfig 拆分出来专注于 Tool 注册
*/
@Configuration
public class ToolConfiguration {
private static final Logger log = LoggerFactory.getLogger(ToolConfiguration.class);
@Bean
public ToolRegistry toolRegistry(TaskManager taskManager, McpManager mcpManager,
ToolContext toolContext, PermissionSettings permissionSettings) {
toolContext.set("TASK_MANAGER", taskManager);
toolContext.set("MCP_MANAGER", mcpManager);
toolContext.set("PERMISSION_SETTINGS", permissionSettings);
ToolRegistry registry = new ToolRegistry();
registry.registerAll(
// 核心工具
new BashTool(),
new FileReadTool(),
new FileWriteTool(),
new FileEditTool(),
new GlobTool(),
new GrepTool(),
new ListFilesTool(),
new WebFetchTool(),
new TodoWriteTool(),
new AgentTool(),
new NotebookEditTool(),
new WebSearchTool(),
new AskUserQuestionTool(),
// 任务管理工具
new TaskCreateTool(),
new TaskGetTool(),
new TaskListTool(),
new TaskUpdateTool(),
new TaskStopTool(),
new TaskOutputTool(),
// 配置/实用工具
new ConfigTool(),
new SleepTool(),
new ToolSearchTool(),
new EnterPlanModeTool(),
new ExitPlanModeTool(),
new SkillTool(),
new SendMessageTool(),
new ListMcpResourcesTool(),
new ReadMcpResourceTool(),
new EnterWorktreeTool(),
new ExitWorktreeTool(),
// 高级工具
new LSPTool(),
new BriefTool(),
new NotificationTool()
);
// MCP 工具桥接
for (var client : mcpManager.getClients().values()) {
for (var mcpTool : client.getTools()) {
registry.register(new McpToolBridge(client.getServerName(), mcpTool));
}
}
toolContext.set("TOOL_REGISTRY", registry);
return registry;
}
}

@ -0,0 +1,53 @@
package com.claudecode.tool;
import java.util.Map;
/**
* 只读工具抽象基类 18+ 只读工具共享的基础实现
* <p>
* 提供:
* <ul>
* <li>{@link #isReadOnly()} 固定返回 true</li>
* <li>{@link #isEnabled()} 默认返回 true</li>
* <li>{@link #checkPermission} 默认 ALLOW</li>
* <li>{@link #errorResult(String)} 标准化错误格式</li>
* </ul>
* 子类只需实现 name/description/inputSchema/execute
*/
public abstract class AbstractReadOnlyTool implements Tool {
@Override
public final boolean isReadOnly() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public PermissionResult checkPermission(Map<String, Object> input, ToolContext context) {
return PermissionResult.ALLOW;
}
@Override
public String activityDescription(Map<String, Object> input) {
return "Reading " + name() + "...";
}
/**
* 标准化错误结果
*/
protected String errorResult(String message) {
return "Error: " + message;
}
/**
* input 中获取必填 String 参数缺失则返回 null 并可由调用方提前 return errorResult
*/
protected String requireParam(Map<String, Object> input, String paramName) {
String err = ToolValidator.requireString(input, paramName);
return err == null ? input.get(paramName).toString() : null;
}
}

@ -0,0 +1,90 @@
package com.claudecode.tool;
import java.nio.file.Path;
/**
* 工具输入验证器 消除 Tool 实现中的重复验证代码
* <p>
* 所有 validate* 方法返回 null 表示验证通过返回 String 表示错误消息
* 典型用法
* <pre>
* String err = ToolValidator.requireString(input, "file_path");
* if (err != null) return err;
* </pre>
*/
public final class ToolValidator {
private ToolValidator() {}
/**
* 验证必填 String 参数 null非空白
* @return null 表示通过否则返回错误消息
*/
public static String requireString(java.util.Map<String, Object> input, String paramName) {
Object value = input.get(paramName);
if (value == null) {
return "Error: '" + paramName + "' is required.";
}
if (value instanceof String s && s.isBlank()) {
return "Error: '" + paramName + "' must not be empty.";
}
return null;
}
/**
* 获取 String 参数带默认值
*/
public static String getString(java.util.Map<String, Object> input, String paramName, String defaultValue) {
Object value = input.get(paramName);
if (value instanceof String s && !s.isBlank()) return s;
return defaultValue;
}
/**
* 安全获取 int 参数带默认值防止 ClassCastException
*/
public static int getInt(java.util.Map<String, Object> input, String paramName, int defaultValue) {
Object value = input.get(paramName);
if (value instanceof Number n) return n.intValue();
if (value instanceof String s) {
try { return Integer.parseInt(s); } catch (NumberFormatException e) { /* fall through */ }
}
return defaultValue;
}
/**
* 安全获取 boolean 参数带默认值
*/
public static boolean getBoolean(java.util.Map<String, Object> input, String paramName, boolean defaultValue) {
Object value = input.get(paramName);
if (value instanceof Boolean b) return b;
if (value instanceof String s) return Boolean.parseBoolean(s);
return defaultValue;
}
/**
* 验证文件路径在工作目录内防止路径遍历
* @return null 表示通过否则返回错误消息
*/
public static String validatePathInWorkDir(String filePath, Path workDir) {
if (filePath == null || filePath.isBlank()) {
return "Error: file path is required.";
}
Path resolved = workDir.resolve(filePath).normalize();
if (!resolved.startsWith(workDir.normalize())) {
return "Error: Path traversal not allowed. Path must be within the working directory.";
}
return null;
}
/**
* 解析并验证路径合并 resolve + normalize + traversal check
* @return 解析后的路径如验证失败则为 null错误通过 errorHolder 返回
*/
public static Path resolveSafePath(String filePath, Path workDir) {
if (filePath == null || filePath.isBlank()) return null;
Path resolved = workDir.resolve(filePath).normalize();
if (!resolved.startsWith(workDir.normalize())) return null;
return resolved;
}
}

@ -2,24 +2,18 @@ package com.claudecode.tool.impl;
import com.claudecode.tool.Tool; import com.claudecode.tool.Tool;
import com.claudecode.tool.ToolContext; import com.claudecode.tool.ToolContext;
import com.claudecode.tool.ToolValidator;
import com.claudecode.tool.util.ProcessExecutor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.awt.*; import java.util.List;
import java.io.IOException;
import java.util.Map; import java.util.Map;
/** /**
* Notification 工具 系统通知 * Notification 工具 系统通知
* <p> * <p>
* 向用户发送系统级通知桌面弹窗 + 可选声音提示用于 * 向用户发送系统级通知桌面弹窗 + 可选声音提示
* <ul>
* <li>长时间任务完成时通知用户</li>
* <li>需要用户注意的错误或警告</li>
* <li>Agent 需要用户输入时的提醒</li>
* </ul>
* <p>
* 底层使用 Java AWT SystemTray (桌面环境) 或退回到 BEL 字符终端环境
*/ */
public class NotificationTool implements Tool { public class NotificationTool implements Tool {
@ -76,24 +70,21 @@ public class NotificationTool implements Tool {
@Override @Override
public String execute(Map<String, Object> input, ToolContext context) { public String execute(Map<String, Object> input, ToolContext context) {
String title = (String) input.get("title"); String err = ToolValidator.requireString(input, "title");
String message = (String) input.get("message"); if (err != null) return err;
String level = (String) input.getOrDefault("level", "info"); err = ToolValidator.requireString(input, "message");
Boolean sound = (Boolean) input.getOrDefault("sound", true); if (err != null) return err;
if (title == null || message == null) { String title = input.get("title").toString();
return "Error: 'title' and 'message' are required"; String message = input.get("message").toString();
} String level = ToolValidator.getString(input, "level", "info");
boolean sound = ToolValidator.getBoolean(input, "sound", true);
// Truncate title if too long title = title.length() > 80 ? title.substring(0, 77) + "..." : title;
if (title.length() > 80) {
title = title.substring(0, 77) + "...";
}
boolean sent = false; boolean sent = false;
String method = "none"; String method = "none";
// Try OS-specific notification
String os = System.getProperty("os.name", "").toLowerCase(); String os = System.getProperty("os.name", "").toLowerCase();
try { try {
if (os.contains("win")) { if (os.contains("win")) {
@ -110,29 +101,19 @@ public class NotificationTool implements Tool {
log.debug("OS notification failed: {}", e.getMessage()); log.debug("OS notification failed: {}", e.getMessage());
} }
// Fallback: terminal bell if (sound) {
if (Boolean.TRUE.equals(sound)) { System.out.print('\u0007');
System.out.print('\u0007'); // BEL character
System.out.flush(); System.out.flush();
} }
if (!sent) { if (!sent) {
// Fallback: just print to terminal
String icon = switch (level) {
case "warning" -> "⚠";
case "error" -> "❌";
default -> "ℹ";
};
method = "terminal"; method = "terminal";
sent = true;
} }
return String.format("Notification sent via %s: [%s] %s - %s", method, level, title, message); return String.format("Notification sent via %s: [%s] %s - %s", method, level, title, message);
} }
private boolean notifyWindows(String title, String message) { private boolean notifyWindows(String title, String message) {
try {
// Use PowerShell to send Windows toast notification
String ps = String.format( String ps = String.format(
"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null; " + "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null; " +
"$n = New-Object System.Windows.Forms.NotifyIcon; " + "$n = New-Object System.Windows.Forms.NotifyIcon; " +
@ -141,40 +122,22 @@ public class NotificationTool implements Tool {
"$n.ShowBalloonTip(5000, '%s', '%s', 'Info'); " + "$n.ShowBalloonTip(5000, '%s', '%s', 'Info'); " +
"Start-Sleep -Seconds 1; $n.Dispose()", "Start-Sleep -Seconds 1; $n.Dispose()",
title.replace("'", "''"), message.replace("'", "''")); title.replace("'", "''"), message.replace("'", "''"));
var result = ProcessExecutor.execute(
ProcessBuilder pb = new ProcessBuilder("powershell", "-NoProfile", "-Command", ps); List.of("powershell", "-NoProfile", "-Command", ps), null, 10000);
pb.inheritIO(); return result.isSuccess();
Process proc = pb.start();
return proc.waitFor() == 0;
} catch (Exception e) {
log.debug("Windows notification failed: {}", e.getMessage());
return false;
}
} }
private boolean notifyMac(String title, String message) { private boolean notifyMac(String title, String message) {
try {
String script = String.format( String script = String.format(
"display notification \"%s\" with title \"%s\"", "display notification \"%s\" with title \"%s\"",
message.replace("\"", "\\\""), title.replace("\"", "\\\"")); message.replace("\"", "\\\""), title.replace("\"", "\\\""));
ProcessBuilder pb = new ProcessBuilder("osascript", "-e", script); var result = ProcessExecutor.execute(List.of("osascript", "-e", script), null, 5000);
Process proc = pb.start(); return result.isSuccess();
return proc.waitFor() == 0;
} catch (Exception e) {
log.debug("macOS notification failed: {}", e.getMessage());
return false;
}
} }
private boolean notifyLinux(String title, String message) { private boolean notifyLinux(String title, String message) {
try { var result = ProcessExecutor.execute(List.of("notify-send", title, message), null, 5000);
ProcessBuilder pb = new ProcessBuilder("notify-send", title, message); return result.isSuccess();
Process proc = pb.start();
return proc.waitFor() == 0;
} catch (Exception e) {
log.debug("Linux notification failed: {}", e.getMessage());
return false;
}
} }
@Override @Override

@ -0,0 +1,115 @@
package com.claudecode.tool.util;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 进程执行器 消除 BashToolGrepToolNotificationTool 中的重复 ProcessBuilder 模式
* <p>
* 封装进程创建超时管理输出捕获和资源清理
*/
public final class ProcessExecutor {
private ProcessExecutor() {}
/**
* 进程执行结果
*/
public record Result(int exitCode, String stdout, String stderr, boolean timedOut) {
public boolean isSuccess() {
return exitCode == 0 && !timedOut;
}
public String output() {
return stdout != null ? stdout : "";
}
public String combinedOutput() {
StringBuilder sb = new StringBuilder();
if (stdout != null && !stdout.isEmpty()) sb.append(stdout);
if (stderr != null && !stderr.isEmpty()) {
if (!sb.isEmpty()) sb.append("\n");
sb.append(stderr);
}
return sb.toString();
}
}
/**
* 在指定工作目录执行命令带超时和资源清理
*/
public static Result execute(List<String> command, Path workDir, long timeoutMs) {
return execute(command, workDir, timeoutMs, null);
}
/**
* 在指定工作目录执行命令带超时环境变量和资源清理
*/
public static Result execute(List<String> command, Path workDir, long timeoutMs, Map<String, String> env) {
ProcessBuilder pb = new ProcessBuilder(command);
if (workDir != null) pb.directory(workDir.toFile());
pb.redirectErrorStream(false);
if (env != null && !env.isEmpty()) {
pb.environment().putAll(env);
}
Process process = null;
try {
process = pb.start();
String stdout;
String stderr;
try (var stdoutStream = process.getInputStream();
var stderrStream = process.getErrorStream()) {
stdout = new String(stdoutStream.readAllBytes());
stderr = new String(stderrStream.readAllBytes());
}
boolean finished = process.waitFor(timeoutMs, TimeUnit.MILLISECONDS);
if (!finished) {
process.destroyForcibly();
process.waitFor(2, TimeUnit.SECONDS);
return new Result(-1, stdout, stderr, true);
}
return new Result(process.exitValue(), stdout, stderr, false);
} catch (IOException e) {
return new Result(-1, "", "IOException: " + e.getMessage(), false);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new Result(-1, "", "Interrupted", false);
} finally {
if (process != null && process.isAlive()) {
process.destroyForcibly();
}
}
}
/**
* 快速检查命令是否可用which/where
*/
public static boolean isCommandAvailable(String command) {
boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win");
String checker = isWindows ? "where" : "which";
Result result = execute(List.of(checker, command), null, 5000);
return result.isSuccess();
}
/**
* 执行 shell 命令通过 sh -c cmd /c
*/
public static Result executeShell(String shellCommand, Path workDir, long timeoutMs) {
boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win");
List<String> command;
if (isWindows) {
command = List.of("cmd.exe", "/c", shellCommand);
} else {
command = List.of("/bin/sh", "-c", shellCommand);
}
return execute(command, workDir, timeoutMs);
}
}
Loading…
Cancel
Save