- /doctor: environment diagnostics (Java, Git, API keys, disk, memory) - /session: session info, save/load/export functionality - /agent: list/detail/stop sub-agents and workers - /rename: session naming with clear/reset support - Registered all 4 commands in AppConfig Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>pull/1/head
parent
5a6798540a
commit
a926b31e34
@ -0,0 +1,204 @@ |
||||
package com.claudecode.command.impl; |
||||
|
||||
import com.claudecode.command.CommandContext; |
||||
import com.claudecode.command.SlashCommand; |
||||
import com.claudecode.console.AnsiStyle; |
||||
import com.claudecode.core.TaskManager; |
||||
|
||||
import java.time.Duration; |
||||
import java.time.Instant; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* /agent 命令 —— Agent 状态管理。 |
||||
* <p> |
||||
* 对应 claude-code 的 agent 管理功能,显示运行中的子 agent/worker 信息。 |
||||
* <ul> |
||||
* <li>/agent —— 列出所有活跃的 agent(任务)</li> |
||||
* <li>/agent [id] —— 查看特定 agent 详情</li> |
||||
* <li>/agent stop [id] —— 停止指定 agent</li> |
||||
* <li>/agent stop-all —— 停止所有 agent</li> |
||||
* </ul> |
||||
*/ |
||||
public class AgentCommand implements SlashCommand { |
||||
|
||||
@Override |
||||
public String name() { |
||||
return "agent"; |
||||
} |
||||
|
||||
@Override |
||||
public String description() { |
||||
return "View and manage sub-agents/workers"; |
||||
} |
||||
|
||||
@Override |
||||
public List<String> aliases() { |
||||
return List.of("agents", "workers"); |
||||
} |
||||
|
||||
@Override |
||||
public String execute(String args, CommandContext context) { |
||||
args = args == null ? "" : args.strip(); |
||||
|
||||
// Get TaskManager from AgentLoop's tool context
|
||||
TaskManager taskManager = getTaskManager(context); |
||||
if (taskManager == null) { |
||||
return AnsiStyle.dim(" No task manager available. Agent management requires an active session."); |
||||
} |
||||
|
||||
if (args.equals("stop-all")) { |
||||
return handleStopAll(taskManager); |
||||
} else if (args.startsWith("stop ")) { |
||||
return handleStop(args.substring(5).strip(), taskManager); |
||||
} else if (!args.isBlank()) { |
||||
return handleDetail(args, taskManager); |
||||
} else { |
||||
return handleList(taskManager); |
||||
} |
||||
} |
||||
|
||||
private String handleList(TaskManager taskManager) { |
||||
var allTasks = taskManager.listTasks(); |
||||
if (allTasks.isEmpty()) { |
||||
return AnsiStyle.dim(" No agents/workers running."); |
||||
} |
||||
|
||||
StringBuilder sb = new StringBuilder(); |
||||
sb.append("\n"); |
||||
sb.append(AnsiStyle.bold(" 🤖 Agents/Workers\n")); |
||||
sb.append(" ").append("─".repeat(60)).append("\n"); |
||||
|
||||
for (var task : allTasks) { |
||||
String statusIcon = switch (task.status()) { |
||||
case RUNNING -> "🟢"; |
||||
case PENDING -> "🟡"; |
||||
case COMPLETED -> "✅"; |
||||
case FAILED -> "❌"; |
||||
case CANCELLED -> "⛔"; |
||||
}; |
||||
|
||||
String age = formatAge(task.createdAt()); |
||||
|
||||
sb.append(" ").append(statusIcon).append(" ") |
||||
.append(AnsiStyle.bold(task.id())) |
||||
.append(" ").append(task.status().name()) |
||||
.append(" ").append(AnsiStyle.dim(age)) |
||||
.append("\n"); |
||||
sb.append(" ").append(truncate(task.description(), 60)).append("\n"); |
||||
} |
||||
|
||||
long running = allTasks.stream() |
||||
.filter(t -> t.status() == TaskManager.TaskStatus.RUNNING).count(); |
||||
long completed = allTasks.stream() |
||||
.filter(t -> t.status() == TaskManager.TaskStatus.COMPLETED).count(); |
||||
|
||||
sb.append("\n ").append(AnsiStyle.dim( |
||||
"Total: " + allTasks.size() + " | Running: " + running + " | Completed: " + completed)); |
||||
sb.append("\n"); |
||||
|
||||
return sb.toString(); |
||||
} |
||||
|
||||
private String handleDetail(String taskId, TaskManager taskManager) { |
||||
var taskOpt = taskManager.getTask(taskId); |
||||
if (taskOpt.isEmpty()) { |
||||
// Try partial match
|
||||
var allTasks = taskManager.listTasks(); |
||||
var matched = allTasks.stream() |
||||
.filter(t -> t.id().startsWith(taskId) || t.description().toLowerCase().contains(taskId.toLowerCase())) |
||||
.findFirst(); |
||||
if (matched.isEmpty()) { |
||||
return AnsiStyle.red(" ✗ No agent found with ID: " + taskId); |
||||
} |
||||
taskOpt = matched; |
||||
} |
||||
|
||||
var task = taskOpt.get(); |
||||
StringBuilder sb = new StringBuilder(); |
||||
sb.append("\n"); |
||||
sb.append(AnsiStyle.bold(" 🤖 Agent Detail: " + task.id() + "\n")); |
||||
sb.append(" ").append("─".repeat(50)).append("\n"); |
||||
sb.append(" ").append(AnsiStyle.dim("Status: ")).append(task.status().name()).append("\n"); |
||||
sb.append(" ").append(AnsiStyle.dim("Task: ")).append(task.description()).append("\n"); |
||||
sb.append(" ").append(AnsiStyle.dim("Created: ")).append(task.createdAt()).append("\n"); |
||||
sb.append(" ").append(AnsiStyle.dim("Updated: ")).append(task.updatedAt()).append("\n"); |
||||
|
||||
if (task.result() != null && !task.result().isBlank()) { |
||||
sb.append(" ").append(AnsiStyle.dim("Result:")).append("\n"); |
||||
task.result().lines().limit(20).forEach(line -> |
||||
sb.append(" ").append(line).append("\n")); |
||||
long totalLines = task.result().lines().count(); |
||||
if (totalLines > 20) { |
||||
sb.append(" ").append(AnsiStyle.dim("... (" + (totalLines - 20) + " more lines)")).append("\n"); |
||||
} |
||||
} |
||||
|
||||
if (!task.metadata().isEmpty()) { |
||||
sb.append(" ").append(AnsiStyle.dim("Metadata:")).append("\n"); |
||||
task.metadata().forEach((k, v) -> |
||||
sb.append(" ").append(k).append(": ").append(v).append("\n")); |
||||
} |
||||
|
||||
return sb.toString(); |
||||
} |
||||
|
||||
private String handleStop(String taskId, TaskManager taskManager) { |
||||
boolean cancelled = taskManager.cancelTask(taskId); |
||||
if (cancelled) { |
||||
return AnsiStyle.green(" ✓ Agent " + taskId + " stopped"); |
||||
} |
||||
|
||||
// Try partial match
|
||||
var allTasks = taskManager.listTasks(); |
||||
var matched = allTasks.stream() |
||||
.filter(t -> t.id().startsWith(taskId)) |
||||
.findFirst(); |
||||
if (matched.isPresent()) { |
||||
cancelled = taskManager.cancelTask(matched.get().id()); |
||||
if (cancelled) { |
||||
return AnsiStyle.green(" ✓ Agent " + matched.get().id() + " stopped"); |
||||
} |
||||
return AnsiStyle.yellow(" ⚠ Agent " + matched.get().id() |
||||
+ " is already " + matched.get().status().name()); |
||||
} |
||||
|
||||
return AnsiStyle.red(" ✗ No running agent found with ID: " + taskId); |
||||
} |
||||
|
||||
private String handleStopAll(TaskManager taskManager) { |
||||
var running = taskManager.listTasks(TaskManager.TaskStatus.RUNNING); |
||||
if (running.isEmpty()) { |
||||
return AnsiStyle.dim(" No running agents to stop."); |
||||
} |
||||
|
||||
int stopped = 0; |
||||
for (var task : running) { |
||||
if (taskManager.cancelTask(task.id())) stopped++; |
||||
} |
||||
|
||||
return AnsiStyle.green(" ✓ Stopped " + stopped + " agent(s)"); |
||||
} |
||||
|
||||
private TaskManager getTaskManager(CommandContext context) { |
||||
// The TaskManager is typically accessible from the tool context
|
||||
// via AgentLoop's tool context. Try to access it.
|
||||
if (context.agentLoop() == null) return null; |
||||
var toolContext = context.agentLoop().getToolContext(); |
||||
if (toolContext == null) return null; |
||||
return toolContext.getOrDefault("TASK_MANAGER", null); |
||||
} |
||||
|
||||
private String formatAge(Instant created) { |
||||
long seconds = Duration.between(created, Instant.now()).getSeconds(); |
||||
if (seconds < 60) return seconds + "s ago"; |
||||
if (seconds < 3600) return (seconds / 60) + "m ago"; |
||||
return (seconds / 3600) + "h " + ((seconds % 3600) / 60) + "m ago"; |
||||
} |
||||
|
||||
private String truncate(String text, int maxLen) { |
||||
if (text == null) return ""; |
||||
if (text.length() <= maxLen) return text; |
||||
return text.substring(0, maxLen - 3) + "..."; |
||||
} |
||||
} |
||||
@ -0,0 +1,166 @@ |
||||
package com.claudecode.command.impl; |
||||
|
||||
import com.claudecode.command.CommandContext; |
||||
import com.claudecode.command.SlashCommand; |
||||
import com.claudecode.console.AnsiStyle; |
||||
|
||||
import java.io.File; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* /doctor 命令 —— 环境诊断。 |
||||
* <p> |
||||
* 对应 claude-code 的 /doctor 命令,检查运行环境是否配置正确。 |
||||
* 检查项: |
||||
* <ul> |
||||
* <li>Java 版本</li> |
||||
* <li>Git 版本和配置</li> |
||||
* <li>环境变量(API key 等)</li> |
||||
* <li>配置文件状态</li> |
||||
* <li>MCP 服务器连接</li> |
||||
* <li>磁盘空间</li> |
||||
* </ul> |
||||
*/ |
||||
public class DoctorCommand implements SlashCommand { |
||||
|
||||
@Override |
||||
public String name() { |
||||
return "doctor"; |
||||
} |
||||
|
||||
@Override |
||||
public String description() { |
||||
return "Run environment diagnostics"; |
||||
} |
||||
|
||||
@Override |
||||
public List<String> aliases() { |
||||
return List.of("diag", "check"); |
||||
} |
||||
|
||||
@Override |
||||
public String execute(String args, CommandContext context) { |
||||
StringBuilder sb = new StringBuilder(); |
||||
sb.append("\n"); |
||||
sb.append(AnsiStyle.bold(" 🩺 Environment Diagnostics\n")); |
||||
sb.append(" ").append("═".repeat(50)).append("\n\n"); |
||||
|
||||
int passed = 0, warned = 0, failed = 0; |
||||
|
||||
// Java Version
|
||||
String javaVersion = System.getProperty("java.version"); |
||||
String javaVendor = System.getProperty("java.vendor"); |
||||
boolean javaOk = javaVersion != null && (javaVersion.startsWith("21") |
||||
|| javaVersion.startsWith("22") || javaVersion.startsWith("23") |
||||
|| javaVersion.startsWith("24") || javaVersion.startsWith("25")); |
||||
sb.append(check("Java", javaVersion + " (" + javaVendor + ")", javaOk)); |
||||
if (javaOk) passed++; else warned++; |
||||
|
||||
// OS Info
|
||||
String osName = System.getProperty("os.name"); |
||||
String osVersion = System.getProperty("os.version"); |
||||
String osArch = System.getProperty("os.arch"); |
||||
sb.append(check("OS", osName + " " + osVersion + " (" + osArch + ")", true)); |
||||
passed++; |
||||
|
||||
// Working Directory
|
||||
String workDir = System.getProperty("user.dir"); |
||||
sb.append(check("Working Dir", workDir, true)); |
||||
passed++; |
||||
|
||||
// Git
|
||||
String gitVersion = runCommand("git", "--version"); |
||||
boolean gitOk = gitVersion != null && gitVersion.contains("git version"); |
||||
sb.append(check("Git", gitOk ? gitVersion.strip() : "not found", gitOk)); |
||||
if (gitOk) passed++; else failed++; |
||||
|
||||
// Git config
|
||||
if (gitOk) { |
||||
String userName = runCommand("git", "config", "user.name"); |
||||
String userEmail = runCommand("git", "config", "user.email"); |
||||
boolean configOk = userName != null && !userName.isBlank() |
||||
&& userEmail != null && !userEmail.isBlank(); |
||||
String configInfo = configOk |
||||
? userName.strip() + " <" + userEmail.strip() + ">" |
||||
: "user.name or user.email not set"; |
||||
sb.append(check("Git Config", configInfo, configOk)); |
||||
if (configOk) passed++; else warned++; |
||||
} |
||||
|
||||
// API Key
|
||||
boolean hasAnthropicKey = System.getenv("ANTHROPIC_API_KEY") != null; |
||||
boolean hasOpenAiKey = System.getenv("OPENAI_API_KEY") != null; |
||||
boolean hasApiKey = hasAnthropicKey || hasOpenAiKey; |
||||
String apiKeyInfo = hasAnthropicKey ? "ANTHROPIC_API_KEY set" |
||||
: hasOpenAiKey ? "OPENAI_API_KEY set" : "No API key found"; |
||||
sb.append(check("API Key", apiKeyInfo, hasApiKey)); |
||||
if (hasApiKey) passed++; else failed++; |
||||
|
||||
// CLAUDE.md
|
||||
boolean hasClaudeMd = new File(workDir, "CLAUDE.md").exists(); |
||||
sb.append(check("CLAUDE.md", hasClaudeMd ? "found" : "not found (optional)", true)); |
||||
passed++; |
||||
|
||||
// Config directory
|
||||
String userHome = System.getProperty("user.home"); |
||||
File configDir = new File(userHome, ".claude-code"); |
||||
boolean hasConfigDir = configDir.exists() && configDir.isDirectory(); |
||||
sb.append(check("Config Dir", configDir.getPath() |
||||
+ (hasConfigDir ? " (exists)" : " (will be created)"), true)); |
||||
passed++; |
||||
|
||||
// MCP config
|
||||
boolean hasProjectMcp = new File(workDir, ".mcp.json").exists(); |
||||
boolean hasGlobalMcp = new File(userHome, ".claude-code-java/mcp.json").exists(); |
||||
String mcpInfo = hasProjectMcp ? ".mcp.json found (project)" |
||||
: hasGlobalMcp ? "mcp.json found (global)" : "no config"; |
||||
sb.append(check("MCP Config", mcpInfo, true)); |
||||
passed++; |
||||
|
||||
// Disk Space
|
||||
File root = new File(workDir); |
||||
long freeSpace = root.getFreeSpace(); |
||||
long totalSpace = root.getTotalSpace(); |
||||
double freeGb = freeSpace / (1024.0 * 1024.0 * 1024.0); |
||||
double totalGb = totalSpace / (1024.0 * 1024.0 * 1024.0); |
||||
boolean diskOk = freeGb > 1.0; |
||||
sb.append(check("Disk Space", String.format("%.1f GB free / %.1f GB total", freeGb, totalGb), diskOk)); |
||||
if (diskOk) passed++; else warned++; |
||||
|
||||
// Memory
|
||||
Runtime runtime = Runtime.getRuntime(); |
||||
long maxMem = runtime.maxMemory() / (1024 * 1024); |
||||
long usedMem = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024); |
||||
sb.append(check("JVM Memory", usedMem + " MB used / " + maxMem + " MB max", maxMem > 256)); |
||||
passed++; |
||||
|
||||
// Summary
|
||||
sb.append("\n ").append("─".repeat(50)).append("\n"); |
||||
sb.append(" ").append(AnsiStyle.bold("Summary: ")); |
||||
sb.append(AnsiStyle.green(passed + " passed")); |
||||
if (warned > 0) sb.append(", ").append(AnsiStyle.yellow(warned + " warnings")); |
||||
if (failed > 0) sb.append(", ").append(AnsiStyle.red(failed + " failed")); |
||||
sb.append("\n"); |
||||
|
||||
return sb.toString(); |
||||
} |
||||
|
||||
private String check(String label, String value, boolean ok) { |
||||
String icon = ok ? AnsiStyle.green("✓") : AnsiStyle.red("✗"); |
||||
return " " + icon + " " + AnsiStyle.bold(String.format("%-14s", label)) |
||||
+ " " + value + "\n"; |
||||
} |
||||
|
||||
private String runCommand(String... cmd) { |
||||
try { |
||||
ProcessBuilder pb = new ProcessBuilder(cmd); |
||||
pb.redirectErrorStream(true); |
||||
Process p = pb.start(); |
||||
String output = new String(p.getInputStream().readAllBytes()).strip(); |
||||
int exit = p.waitFor(); |
||||
return exit == 0 ? output : null; |
||||
} catch (Exception e) { |
||||
return null; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,74 @@ |
||||
package com.claudecode.command.impl; |
||||
|
||||
import com.claudecode.command.CommandContext; |
||||
import com.claudecode.command.SlashCommand; |
||||
import com.claudecode.console.AnsiStyle; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* /rename 命令 —— 会话重命名。 |
||||
* <p> |
||||
* 对应 claude-code 的 /rename 命令,给当前会话设置一个友好名称。 |
||||
* 会话名称在 UI 标题栏和会话列表中显示。 |
||||
*/ |
||||
public class RenameCommand implements SlashCommand { |
||||
|
||||
/** 当前会话名称(进程级别) */ |
||||
private static volatile String sessionName = null; |
||||
|
||||
@Override |
||||
public String name() { |
||||
return "rename"; |
||||
} |
||||
|
||||
@Override |
||||
public String description() { |
||||
return "Rename the current session"; |
||||
} |
||||
|
||||
@Override |
||||
public List<String> aliases() { |
||||
return List.of("name", "title"); |
||||
} |
||||
|
||||
@Override |
||||
public String execute(String args, CommandContext context) { |
||||
args = args == null ? "" : args.strip(); |
||||
|
||||
if (args.isBlank()) { |
||||
if (sessionName == null) { |
||||
return AnsiStyle.dim(" No session name set. Use /rename <name> to set one."); |
||||
} |
||||
return " Current session name: " + AnsiStyle.bold(sessionName); |
||||
} |
||||
|
||||
if (args.equals("clear") || args.equals("reset")) { |
||||
sessionName = null; |
||||
return AnsiStyle.green(" ✓ Session name cleared"); |
||||
} |
||||
|
||||
// Validate name
|
||||
if (args.length() > 100) { |
||||
return AnsiStyle.yellow(" ⚠ Session name too long (max 100 characters)"); |
||||
} |
||||
|
||||
String oldName = sessionName; |
||||
sessionName = args; |
||||
|
||||
if (oldName != null) { |
||||
return AnsiStyle.green(" ✓ Session renamed: " + oldName + " → " + sessionName); |
||||
} |
||||
return AnsiStyle.green(" ✓ Session named: " + sessionName); |
||||
} |
||||
|
||||
/** 获取当前会话名称(供其他组件使用) */ |
||||
public static String getSessionName() { |
||||
return sessionName; |
||||
} |
||||
|
||||
/** 设置会话名称(供程序化设置) */ |
||||
public static void setSessionName(String name) { |
||||
sessionName = name; |
||||
} |
||||
} |
||||
@ -0,0 +1,210 @@ |
||||
package com.claudecode.command.impl; |
||||
|
||||
import com.claudecode.command.CommandContext; |
||||
import com.claudecode.command.SlashCommand; |
||||
import com.claudecode.console.AnsiStyle; |
||||
|
||||
import java.io.IOException; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.time.Instant; |
||||
import java.time.ZoneId; |
||||
import java.time.format.DateTimeFormatter; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* /session 命令 —— 会话管理。 |
||||
* <p> |
||||
* 显示当前会话信息,支持: |
||||
* <ul> |
||||
* <li>/session —— 显示当前会话摘要</li> |
||||
* <li>/session save [name] —— 保存会话快照</li> |
||||
* <li>/session list —— 列出保存的会话</li> |
||||
* <li>/session export —— 导出会话为 JSON</li> |
||||
* </ul> |
||||
*/ |
||||
public class SessionCommand implements SlashCommand { |
||||
|
||||
private static final DateTimeFormatter FMT = |
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") |
||||
.withZone(ZoneId.systemDefault()); |
||||
|
||||
private final Instant startTime = Instant.now(); |
||||
|
||||
@Override |
||||
public String name() { |
||||
return "session"; |
||||
} |
||||
|
||||
@Override |
||||
public String description() { |
||||
return "View and manage the current session"; |
||||
} |
||||
|
||||
@Override |
||||
public List<String> aliases() { |
||||
return List.of("sess"); |
||||
} |
||||
|
||||
@Override |
||||
public String execute(String args, CommandContext context) { |
||||
args = args == null ? "" : args.strip(); |
||||
|
||||
if (args.startsWith("save")) { |
||||
String sessionName = args.length() > 5 ? args.substring(5).strip() : ""; |
||||
return handleSave(sessionName, context); |
||||
} else if (args.equals("list")) { |
||||
return handleList(); |
||||
} else if (args.equals("export")) { |
||||
return handleExport(context); |
||||
} else { |
||||
return showSessionInfo(context); |
||||
} |
||||
} |
||||
|
||||
private String showSessionInfo(CommandContext context) { |
||||
StringBuilder sb = new StringBuilder(); |
||||
sb.append("\n"); |
||||
sb.append(AnsiStyle.bold(" 📋 Session Info\n")); |
||||
sb.append(" ").append("─".repeat(50)).append("\n"); |
||||
|
||||
sb.append(" ").append(AnsiStyle.dim("Started: ")).append(FMT.format(startTime)).append("\n"); |
||||
|
||||
// Duration
|
||||
long seconds = java.time.Duration.between(startTime, Instant.now()).getSeconds(); |
||||
String duration; |
||||
if (seconds < 60) duration = seconds + "s"; |
||||
else if (seconds < 3600) duration = (seconds / 60) + "m " + (seconds % 60) + "s"; |
||||
else duration = (seconds / 3600) + "h " + ((seconds % 3600) / 60) + "m"; |
||||
sb.append(" ").append(AnsiStyle.dim("Duration: ")).append(duration).append("\n"); |
||||
|
||||
sb.append(" ").append(AnsiStyle.dim("Work Dir: ")).append(System.getProperty("user.dir")).append("\n"); |
||||
|
||||
// Token usage (from AgentLoop's TokenTracker)
|
||||
var agentLoop = context.agentLoop(); |
||||
if (agentLoop != null && agentLoop.getAutoCompactManager() != null) { |
||||
sb.append(" ").append(AnsiStyle.dim("Auto-compact: ")).append("enabled").append("\n"); |
||||
} |
||||
|
||||
// Tool count
|
||||
int toolCount = context.toolRegistry() != null ? context.toolRegistry().size() : 0; |
||||
sb.append(" ").append(AnsiStyle.dim("Tools: ")).append(toolCount).append(" registered\n"); |
||||
|
||||
// Command count
|
||||
int cmdCount = context.commandRegistry() != null ? context.commandRegistry().getCommands().size() : 0; |
||||
sb.append(" ").append(AnsiStyle.dim("Commands: ")).append(cmdCount).append(" available\n"); |
||||
|
||||
// Session memory
|
||||
Path memDir = getSessionDir(); |
||||
boolean hasMemory = memDir != null && Files.exists(memDir.resolve("SESSION_MEMORY.md")); |
||||
sb.append(" ").append(AnsiStyle.dim("Session Memory: ")) |
||||
.append(hasMemory ? "active" : "not yet created").append("\n"); |
||||
|
||||
sb.append("\n ").append(AnsiStyle.dim("Use /session save [name] to save, /session list to view saved")).append("\n"); |
||||
|
||||
return sb.toString(); |
||||
} |
||||
|
||||
private String handleSave(String sessionName, CommandContext context) { |
||||
Path sessionsDir = getSessionsDir(); |
||||
if (sessionsDir == null) { |
||||
return AnsiStyle.red(" ✗ Cannot determine sessions directory"); |
||||
} |
||||
|
||||
try { |
||||
Files.createDirectories(sessionsDir); |
||||
} catch (IOException e) { |
||||
return AnsiStyle.red(" ✗ Cannot create sessions directory: " + e.getMessage()); |
||||
} |
||||
|
||||
if (sessionName.isBlank()) { |
||||
sessionName = "session-" + FMT.format(Instant.now()).replace(" ", "_").replace(":", "-"); |
||||
} |
||||
|
||||
Path sessionFile = sessionsDir.resolve(sessionName + ".md"); |
||||
try { |
||||
StringBuilder content = new StringBuilder(); |
||||
content.append("# Session: ").append(sessionName).append("\n\n"); |
||||
content.append("- Started: ").append(FMT.format(startTime)).append("\n"); |
||||
content.append("- Saved: ").append(FMT.format(Instant.now())).append("\n"); |
||||
content.append("- Working Directory: ").append(System.getProperty("user.dir")).append("\n"); |
||||
content.append("\n## Notes\n\n"); |
||||
content.append("(Add session notes here)\n"); |
||||
|
||||
Files.writeString(sessionFile, content.toString(), StandardCharsets.UTF_8); |
||||
return AnsiStyle.green(" ✓ Session saved: " + sessionFile); |
||||
} catch (IOException e) { |
||||
return AnsiStyle.red(" ✗ Failed to save session: " + e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
private String handleList() { |
||||
Path sessionsDir = getSessionsDir(); |
||||
if (sessionsDir == null || !Files.exists(sessionsDir)) { |
||||
return AnsiStyle.dim(" No saved sessions."); |
||||
} |
||||
|
||||
StringBuilder sb = new StringBuilder(); |
||||
sb.append("\n"); |
||||
sb.append(AnsiStyle.bold(" 📁 Saved Sessions\n")); |
||||
sb.append(" ").append("─".repeat(50)).append("\n"); |
||||
|
||||
try (var stream = Files.list(sessionsDir)) { |
||||
var files = stream |
||||
.filter(f -> f.toString().endsWith(".md")) |
||||
.sorted() |
||||
.toList(); |
||||
|
||||
if (files.isEmpty()) { |
||||
return AnsiStyle.dim(" No saved sessions."); |
||||
} |
||||
|
||||
for (var file : files) { |
||||
String name = file.getFileName().toString().replace(".md", ""); |
||||
long size = Files.size(file); |
||||
sb.append(" • ").append(name).append(" (").append(size).append(" bytes)\n"); |
||||
} |
||||
} catch (IOException e) { |
||||
return AnsiStyle.red(" ✗ Error listing sessions: " + e.getMessage()); |
||||
} |
||||
|
||||
return sb.toString(); |
||||
} |
||||
|
||||
private String handleExport(CommandContext context) { |
||||
Path exportFile = Path.of(System.getProperty("user.dir"), "session-export.json"); |
||||
try { |
||||
StringBuilder json = new StringBuilder(); |
||||
json.append("{\n"); |
||||
json.append(" \"started\": \"").append(startTime).append("\",\n"); |
||||
json.append(" \"exported\": \"").append(Instant.now()).append("\",\n"); |
||||
json.append(" \"workDir\": \"") |
||||
.append(System.getProperty("user.dir").replace("\\", "\\\\")).append("\",\n"); |
||||
json.append(" \"tools\": ").append(context.toolRegistry() != null |
||||
? context.toolRegistry().size() : 0).append(",\n"); |
||||
json.append(" \"commands\": ").append(context.commandRegistry() != null |
||||
? context.commandRegistry().getCommands().size() : 0).append("\n"); |
||||
json.append("}\n"); |
||||
|
||||
Files.writeString(exportFile, json.toString(), StandardCharsets.UTF_8); |
||||
return AnsiStyle.green(" ✓ Session exported to: " + exportFile); |
||||
} catch (IOException e) { |
||||
return AnsiStyle.red(" ✗ Export failed: " + e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
private Path getSessionsDir() { |
||||
String userHome = System.getProperty("user.home"); |
||||
if (userHome == null) return null; |
||||
return Path.of(userHome, ".claude", "sessions"); |
||||
} |
||||
|
||||
private Path getSessionDir() { |
||||
String workDir = System.getProperty("user.dir"); |
||||
String sanitized = Path.of(workDir).toAbsolutePath().toString() |
||||
.replace(":", "_").replace("\\", "_").replace("/", "_"); |
||||
return Path.of(System.getProperty("user.home")) |
||||
.resolve(".claude").resolve("projects").resolve(sanitized); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue