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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.TokenEstimationService;
|
||||
@@ -24,8 +25,7 @@ public class ContextVizCommand implements SlashCommand {
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 📊 Context Window Visualization\n"));
|
||||
sb.append(" ").append("─".repeat(45)).append("\n\n");
|
||||
sb.append(CommandUtils.header("📊", "Context Window Visualization"));
|
||||
|
||||
if (context.agentLoop() == null) {
|
||||
return sb.append(" No active agent loop\n").toString();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.TokenTracker;
|
||||
@@ -29,9 +30,7 @@ public class CostCommand implements SlashCommand {
|
||||
int msgCount = context.agentLoop().getMessageHistory().size();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 💰 Token Usage & Cost\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
sb.append(CommandUtils.header("💰", "Token Usage & Cost"));
|
||||
|
||||
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");
|
||||
@@ -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("─".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("Est. Cost: ")).append(AnsiStyle.green("$" + String.format("%.4f", tracker.estimateCost()))).append("\n");
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.InternalLogger;
|
||||
@@ -23,11 +24,10 @@ public class DebugCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
String trimmed = CommandUtils.parseArgs(args);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 🐛 Debug Mode\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🐛", "Debug Mode"));
|
||||
|
||||
if (context.agentLoop() == null) {
|
||||
return sb.append(" No active agent loop\n").toString();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
@@ -23,32 +23,28 @@ public class EnvCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
String trimmed = CommandUtils.parseArgs(args);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 🔧 Environment\n"));
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🔧", "Environment"));
|
||||
|
||||
// System info
|
||||
sb.append(AnsiStyle.bold(" System\n"));
|
||||
sb.append(CommandUtils.subtitle("System")).append("\n");
|
||||
sb.append(" OS: ").append(System.getProperty("os.name")).append(" ")
|
||||
.append(System.getProperty("os.version")).append("\n");
|
||||
sb.append(" Java: ").append(System.getProperty("java.version"))
|
||||
.append(" (").append(System.getProperty("java.vendor")).append(")\n");
|
||||
sb.append(" JVM: ").append(System.getProperty("java.vm.name")).append("\n");
|
||||
sb.append(" Heap: ").append(formatBytes(Runtime.getRuntime().totalMemory()))
|
||||
.append(" / ").append(formatBytes(Runtime.getRuntime().maxMemory())).append("\n");
|
||||
sb.append(" Heap: ").append(CommandUtils.formatBytes(Runtime.getRuntime().totalMemory()))
|
||||
.append(" / ").append(CommandUtils.formatBytes(Runtime.getRuntime().maxMemory())).append("\n");
|
||||
sb.append(" PID: ").append(ProcessHandle.current().pid()).append("\n\n");
|
||||
|
||||
// Work directory
|
||||
sb.append(AnsiStyle.bold(" Paths\n"));
|
||||
sb.append(CommandUtils.subtitle("Paths")).append("\n");
|
||||
sb.append(" WorkDir: ").append(System.getProperty("user.dir")).append("\n");
|
||||
sb.append(" Home: ").append(System.getProperty("user.home")).append("\n");
|
||||
sb.append(" Config: ").append(System.getProperty("user.home"))
|
||||
.append(File.separator).append(".claude-code-java").append("\n\n");
|
||||
|
||||
// Relevant env vars
|
||||
sb.append(AnsiStyle.bold(" Environment Variables\n"));
|
||||
sb.append(CommandUtils.subtitle("Environment Variables")).append("\n");
|
||||
List<String> relevantVars = List.of(
|
||||
"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CLAUDE_CODE_",
|
||||
"JAVA_HOME", "PATH", "SHELL", "TERM", "EDITOR"
|
||||
@@ -57,24 +53,14 @@ public class EnvCommand implements SlashCommand {
|
||||
Map<String, String> env = new TreeMap<>(System.getenv());
|
||||
for (Map.Entry<String, String> entry : env.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
boolean show = false;
|
||||
for (String prefix : relevantVars) {
|
||||
if (key.startsWith(prefix) || key.equals(prefix)) {
|
||||
show = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
boolean show = relevantVars.stream().anyMatch(p -> key.startsWith(p) || key.equals(p));
|
||||
if (!show && !trimmed.equals("all")) continue;
|
||||
|
||||
String value = entry.getValue();
|
||||
// Mask secrets
|
||||
if (key.contains("KEY") || key.contains("SECRET") || key.contains("TOKEN")) {
|
||||
value = value.length() > 8 ? value.substring(0, 4) + "****" + value.substring(value.length() - 4) : "****";
|
||||
}
|
||||
// Truncate long values
|
||||
if (value.length() > 80) {
|
||||
value = value.substring(0, 77) + "...";
|
||||
}
|
||||
value = CommandUtils.truncate(value, 80);
|
||||
sb.append(" ").append(AnsiStyle.cyan(key)).append("=").append(value).append("\n");
|
||||
}
|
||||
|
||||
@@ -84,10 +70,4 @@ public class EnvCommand implements SlashCommand {
|
||||
|
||||
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;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.MemoryPoolMXBean;
|
||||
@@ -12,11 +12,9 @@ import java.lang.management.MemoryUsage;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /heapdump 命令 —— JVM 堆转储(Java 独有优势)。
|
||||
* 使用 HotSpotDiagnosticMXBean 进行堆转储。
|
||||
*/
|
||||
public class HeapdumpCommand implements SlashCommand {
|
||||
|
||||
@@ -28,45 +26,39 @@ public class HeapdumpCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
String trimmed = CommandUtils.parseArgs(args);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 📦 JVM Heap Dump\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
sb.append(CommandUtils.header("📦", "JVM Heap Dump"));
|
||||
|
||||
if (trimmed.equals("info") || trimmed.isEmpty()) {
|
||||
// Show memory pool details
|
||||
MemoryMXBean memBean = ManagementFactory.getMemoryMXBean();
|
||||
MemoryUsage heap = memBean.getHeapMemoryUsage();
|
||||
MemoryUsage nonHeap = memBean.getNonHeapMemoryUsage();
|
||||
|
||||
sb.append(AnsiStyle.bold(" Heap Memory\n"));
|
||||
sb.append(" Used: ").append(formatBytes(heap.getUsed())).append("\n");
|
||||
sb.append(" Committed: ").append(formatBytes(heap.getCommitted())).append("\n");
|
||||
sb.append(" Max: ").append(formatBytes(heap.getMax())).append("\n\n");
|
||||
sb.append(CommandUtils.subtitle("Heap Memory")).append("\n");
|
||||
sb.append(" Used: ").append(CommandUtils.formatBytes(heap.getUsed())).append("\n");
|
||||
sb.append(" Committed: ").append(CommandUtils.formatBytes(heap.getCommitted())).append("\n");
|
||||
sb.append(" Max: ").append(CommandUtils.formatBytes(heap.getMax())).append("\n\n");
|
||||
|
||||
sb.append(AnsiStyle.bold(" Non-Heap Memory\n"));
|
||||
sb.append(" Used: ").append(formatBytes(nonHeap.getUsed())).append("\n");
|
||||
sb.append(" Committed: ").append(formatBytes(nonHeap.getCommitted())).append("\n\n");
|
||||
sb.append(CommandUtils.subtitle("Non-Heap Memory")).append("\n");
|
||||
sb.append(" Used: ").append(CommandUtils.formatBytes(nonHeap.getUsed())).append("\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()) {
|
||||
MemoryUsage usage = pool.getUsage();
|
||||
if (usage != null && usage.getUsed() > 0) {
|
||||
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"));
|
||||
|
||||
} else if (trimmed.startsWith("dump")) {
|
||||
// Determine output path
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
|
||||
String filename = trimmed.length() > 5 ? trimmed.substring(5).trim() : "";
|
||||
if (filename.isEmpty()) {
|
||||
filename = "heapdump-" + timestamp + ".hprof";
|
||||
}
|
||||
if (filename.isEmpty()) filename = "heapdump-" + timestamp + ".hprof";
|
||||
Path dumpPath = Path.of(System.getProperty("user.dir"), filename);
|
||||
|
||||
try {
|
||||
@@ -74,29 +66,28 @@ public class HeapdumpCommand implements SlashCommand {
|
||||
com.sun.management.HotSpotDiagnosticMXBean.class);
|
||||
hotspot.dumpHeap(dumpPath.toString(), true);
|
||||
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(" 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"));
|
||||
} 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)"));
|
||||
}
|
||||
|
||||
} else if (trimmed.equals("gc")) {
|
||||
// Trigger GC and report
|
||||
long beforeUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
|
||||
System.gc();
|
||||
long afterUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
|
||||
long freed = beforeUsed - afterUsed;
|
||||
|
||||
sb.append(" 🗑 Garbage collection triggered\n");
|
||||
sb.append(" Before: ").append(formatBytes(beforeUsed)).append("\n");
|
||||
sb.append(" After: ").append(formatBytes(afterUsed)).append("\n");
|
||||
sb.append(" Freed: ").append(AnsiStyle.green(formatBytes(Math.max(0, freed)))).append("\n");
|
||||
sb.append(" Before: ").append(CommandUtils.formatBytes(beforeUsed)).append("\n");
|
||||
sb.append(" After: ").append(CommandUtils.formatBytes(afterUsed)).append("\n");
|
||||
sb.append(" Freed: ").append(AnsiStyle.green(CommandUtils.formatBytes(Math.max(0, freed)))).append("\n");
|
||||
|
||||
} 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 dump Generate .hprof file\n");
|
||||
sb.append(" /heapdump gc Trigger garbage collection\n");
|
||||
@@ -104,11 +95,4 @@ public class HeapdumpCommand implements SlashCommand {
|
||||
|
||||
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;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.mcp.McpClient;
|
||||
@@ -47,7 +48,7 @@ public class McpCommand implements SlashCommand {
|
||||
return AnsiStyle.red(" ❌ MCP manager not initialized");
|
||||
}
|
||||
|
||||
String trimmed = args.strip();
|
||||
String trimmed = CommandUtils.parseArgs(args);
|
||||
if (trimmed.isEmpty()) {
|
||||
return showStatus(manager);
|
||||
}
|
||||
@@ -72,9 +73,7 @@ public class McpCommand implements SlashCommand {
|
||||
*/
|
||||
private String showStatus(McpManager manager) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🔌 MCP Server Status\n"));
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🔌", "MCP Server Status"));
|
||||
|
||||
Map<String, McpClient> clients = manager.getClients();
|
||||
if (clients.isEmpty()) {
|
||||
@@ -159,7 +158,7 @@ public class McpCommand implements SlashCommand {
|
||||
for (McpClient.McpTool tool : client.getTools()) {
|
||||
sb.append(" • ").append(tool.name());
|
||||
if (!tool.description().isEmpty()) {
|
||||
sb.append(AnsiStyle.dim(" - " + truncate(tool.description(), 60)));
|
||||
sb.append(AnsiStyle.dim(" - " + CommandUtils.truncate(tool.description(), 60)));
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
@@ -193,9 +192,7 @@ public class McpCommand implements SlashCommand {
|
||||
*/
|
||||
private String handleTools(McpManager manager, String args) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🛠️ MCP Tools\n"));
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🛠️", "MCP Tools"));
|
||||
|
||||
String serverFilter = args.isEmpty() ? null : args.split("\\s+")[0];
|
||||
|
||||
@@ -220,7 +217,7 @@ public class McpCommand implements SlashCommand {
|
||||
}
|
||||
if (tool.inputSchema() != null) {
|
||||
sb.append(" ").append(AnsiStyle.dim("Schema: " +
|
||||
truncate(tool.inputSchema().toString(), 80))).append("\n");
|
||||
CommandUtils.truncate(tool.inputSchema().toString(), 80))).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
@@ -234,9 +231,7 @@ public class McpCommand implements SlashCommand {
|
||||
*/
|
||||
private String handleResources(McpManager manager, String args) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 📦 MCP Resources\n"));
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
sb.append(CommandUtils.header("📦", "MCP Resources"));
|
||||
|
||||
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 持有者(简单单例,供命令和其他组件访问) ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.telemetry.MetricsCollector;
|
||||
@@ -24,45 +25,40 @@ public class PerformanceCommand implements SlashCommand {
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" ⚡ Performance Statistics\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
sb.append(CommandUtils.header("⚡", "Performance Statistics"));
|
||||
|
||||
// JVM stats
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
long totalMem = runtime.totalMemory();
|
||||
long freeMem = runtime.freeMemory();
|
||||
long usedMem = totalMem - freeMem;
|
||||
long maxMem = runtime.maxMemory();
|
||||
|
||||
sb.append(AnsiStyle.bold(" Memory\n"));
|
||||
sb.append(" Used: ").append(formatBytes(usedMem)).append("\n");
|
||||
sb.append(" Allocated: ").append(formatBytes(totalMem)).append("\n");
|
||||
sb.append(" Max: ").append(formatBytes(maxMem)).append("\n");
|
||||
sb.append(" Usage: ").append(memBar(usedMem, maxMem)).append("\n\n");
|
||||
sb.append(CommandUtils.subtitle("Memory")).append("\n");
|
||||
sb.append(" Used: ").append(CommandUtils.formatBytes(usedMem)).append("\n");
|
||||
sb.append(" Allocated: ").append(CommandUtils.formatBytes(totalMem)).append("\n");
|
||||
sb.append(" Max: ").append(CommandUtils.formatBytes(maxMem)).append("\n");
|
||||
sb.append(" Usage: ").append(CommandUtils.progressBar((double) usedMem / maxMem, 20)).append("\n\n");
|
||||
|
||||
// Thread stats
|
||||
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(" Available: ").append(runtime.availableProcessors()).append(" CPUs\n\n");
|
||||
|
||||
// GC stats
|
||||
long gcCount = 0;
|
||||
long gcTime = 0;
|
||||
for (var gc : java.lang.management.ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
gcCount += gc.getCollectionCount();
|
||||
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(" 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) {
|
||||
Object metricsObj = context.agentLoop().getToolContext().get("METRICS_COLLECTOR");
|
||||
if (metricsObj instanceof MetricsCollector metrics) {
|
||||
sb.append(AnsiStyle.bold(" Session Metrics\n"));
|
||||
sb.append(" Duration: ").append(formatDuration(metrics.getSessionDurationSeconds())).append("\n");
|
||||
sb.append(CommandUtils.subtitle("Session Metrics")).append("\n");
|
||||
sb.append(" Duration: ").append(CommandUtils.formatDuration(metrics.getSessionDurationSeconds())).append("\n");
|
||||
var toolUsage = metrics.getToolUsage();
|
||||
if (!toolUsage.isEmpty()) {
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.plugin.*;
|
||||
@@ -55,7 +56,7 @@ public class PluginCommand implements SlashCommand {
|
||||
return AnsiStyle.red(" ✗ Plugin system not initialized");
|
||||
}
|
||||
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
String trimmed = CommandUtils.parseArgs(args);
|
||||
|
||||
// 无参数:列出所有插件
|
||||
if (trimmed.isEmpty()) {
|
||||
@@ -87,9 +88,7 @@ public class PluginCommand implements SlashCommand {
|
||||
private String listPlugins(PluginManager manager) {
|
||||
List<PluginInfo> plugins = manager.getPlugins();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🔌 Loaded Plugins")).append("\n");
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🔌", "Loaded Plugins"));
|
||||
|
||||
if (plugins.isEmpty()) {
|
||||
sb.append(AnsiStyle.dim(" No plugins loaded.")).append("\n");
|
||||
@@ -174,9 +173,7 @@ public class PluginCommand implements SlashCommand {
|
||||
|
||||
Plugin p = info.plugin();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🔌 Plugin Details")).append("\n");
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🔌", "Plugin Details"));
|
||||
|
||||
sb.append(" ").append(AnsiStyle.bold("Name: ")).append(p.name()).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("ID: ")).append(AnsiStyle.cyan(p.id())).append("\n");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
@@ -25,11 +26,10 @@ public class ReleaseNotesCommand implements SlashCommand {
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 📋 Release Notes\n"));
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
sb.append(CommandUtils.header("📋", "Release Notes"));
|
||||
|
||||
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(" • BriefTool: output verbosity control\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(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(" • Git Worktree: parallel branch isolation for agent tasks\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(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(" • Skills execution system with /skill command\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(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(" • 8 tool description improvements\n");
|
||||
sb.append(" • New tools: TaskStop, TaskOutput, Sleep, ToolSearch\n");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.RateLimiter;
|
||||
@@ -24,8 +25,7 @@ public class ResetLimitsCommand implements SlashCommand {
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 🔄 Rate Limit Reset\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🔄", "Rate Limit Reset"));
|
||||
|
||||
if (context.agentLoop() == null) {
|
||||
return sb.append(" No active agent loop\n").toString();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
@@ -20,11 +21,10 @@ public class SandboxCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
String trimmed = CommandUtils.parseArgs(args);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 🏖 Sandbox Mode\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🏖", "Sandbox Mode"));
|
||||
|
||||
if (context.agentLoop() == null) {
|
||||
return sb.append(" No active agent loop\n").toString();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.context.SkillLoader;
|
||||
@@ -50,9 +51,7 @@ public class SkillsCommand implements SlashCommand {
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🎯 Available Skills\n"));
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🎯", "Available Skills"));
|
||||
|
||||
if (skills.isEmpty()) {
|
||||
sb.append(AnsiStyle.dim(" (No available skills)\n\n"));
|
||||
@@ -90,9 +89,7 @@ public class SkillsCommand implements SlashCommand {
|
||||
|
||||
private String formatSkillDetail(SkillLoader.Skill skill) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🎯 Skill: " + skill.name())).append("\n");
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🎯", "Skill: " + skill.name()));
|
||||
|
||||
sb.append(" ").append(AnsiStyle.bold("Source: ")).append(skill.source()).append("\n");
|
||||
if (!skill.description().isEmpty()) {
|
||||
@@ -106,9 +103,7 @@ public class SkillsCommand implements SlashCommand {
|
||||
|
||||
// Show content preview
|
||||
String content = skill.content();
|
||||
if (content.length() > 500) {
|
||||
content = content.substring(0, 497) + "...";
|
||||
}
|
||||
content = CommandUtils.truncate(content, 500);
|
||||
sb.append(AnsiStyle.dim(" Content:\n"));
|
||||
for (String line : content.lines().toList()) {
|
||||
sb.append(AnsiStyle.dim(" │ " + line)).append("\n");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.TokenTracker;
|
||||
@@ -31,9 +32,7 @@ public class StatusCommand implements SlashCommand {
|
||||
@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");
|
||||
sb.append(CommandUtils.header("📊", "Session Status"));
|
||||
|
||||
// 模型信息
|
||||
TokenTracker tracker = context.agentLoop().getTokenTracker();
|
||||
@@ -68,7 +67,7 @@ public class StatusCommand implements SlashCommand {
|
||||
|
||||
// 运行时间
|
||||
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 版本
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.TaskManager;
|
||||
@@ -38,7 +39,7 @@ public class TasksCommand implements SlashCommand {
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
List<TaskInfo> tasks;
|
||||
String filter = (args == null) ? "" : args.strip();
|
||||
String filter = CommandUtils.parseArgs(args);
|
||||
|
||||
// Optional status filter
|
||||
if (!filter.isEmpty()) {
|
||||
@@ -59,7 +60,7 @@ public class TasksCommand implements SlashCommand {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
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) {
|
||||
String icon = switch (task.status()) {
|
||||
@@ -84,30 +85,21 @@ public class TasksCommand implements SlashCommand {
|
||||
.append(task.description()).append("\n");
|
||||
|
||||
// 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"));
|
||||
|
||||
// Result preview for completed/failed
|
||||
if (task.result() != null) {
|
||||
String preview = task.result().length() > 60
|
||||
? task.result().substring(0, 57) + "..."
|
||||
: task.result();
|
||||
String preview = CommandUtils.truncate(task.result(), 60);
|
||||
sb.append(" ").append(AnsiStyle.dim("→ " + preview));
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
// Summary
|
||||
sb.append(" ").append("─".repeat(60)).append("\n");
|
||||
sb.append(CommandUtils.separator(60)).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.dim(taskManager.getSummary())).append("\n");
|
||||
|
||||
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;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
@@ -23,11 +24,10 @@ public class TraceCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
String trimmed = CommandUtils.parseArgs(args);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n").append(AnsiStyle.bold(" 🔍 Request Tracing\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🔍", "Request Tracing"));
|
||||
|
||||
if (context.agentLoop() == null) {
|
||||
return sb.append(" No active agent loop\n").toString();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.CommandUtils;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
@@ -28,9 +29,7 @@ public class VersionCommand implements SlashCommand {
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🏷️ Claude Code Java\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
sb.append(CommandUtils.header("🏷️", "Claude Code Java"));
|
||||
|
||||
sb.append(" ").append(AnsiStyle.bold("Version: "))
|
||||
.append(AnsiStyle.cyan("v" + VERSION)).append("\n");
|
||||
|
||||
Reference in New Issue
Block a user