Spaces:
Sleeping
Sleeping
| /** | |
| * SSE streaming endpoint for the chat agent. | |
| * This is a raw Express route (not tRPC) because tRPC doesn't support SSE streaming well. | |
| */ | |
| import { Request, Response } from "express"; | |
| import { runAgentLoop } from "./agent"; | |
| import * as db from "../db"; | |
| import { buildSystemPrompt } from "./system-prompt"; | |
| import { getToolList, getPlanMode, setPlanMode, addPlanStep, updatePlanStep, getEffortLevel, setEffortLevel, getSessionSkills, getSessionTasks } from "../tools/executor"; | |
| // Simple auth check β reuse the session cookie, with dev-mode bypass | |
| async function getAuthUser(req: Request) { | |
| // Dev mode: auto-authenticate as dev user (same as tRPC context) | |
| if (process.env.DEV_MODE === "true" || !process.env.OAUTH_SERVER_URL) { | |
| const user = await db.getUserByOpenId("dev-user"); | |
| if (user) return user; | |
| // Seed dev user if missing | |
| await db.upsertUser({ | |
| openId: "dev-user", | |
| name: "Developer", | |
| email: "dev@claw.local", | |
| role: "admin", | |
| }); | |
| return await db.getUserByOpenId("dev-user") || null; | |
| } | |
| // Production mode: use OAuth SDK | |
| try { | |
| const { sdk } = await import("../_core/sdk"); | |
| return await sdk.authenticateRequest(req); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| /** | |
| * POST /api/chat/stream | |
| * Body: { sessionId: number, message: string } | |
| * Response: SSE stream | |
| */ | |
| export async function handleChatStream(req: Request, res: Response) { | |
| const user = await getAuthUser(req); | |
| if (!user) { | |
| res.status(401).json({ error: "Unauthorized" }); | |
| return; | |
| } | |
| const { sessionId, message } = req.body; | |
| if (!sessionId || !message) { | |
| res.status(400).json({ error: "sessionId and message are required" }); | |
| return; | |
| } | |
| // Verify session ownership | |
| const session = await db.getSession(sessionId, user.id); | |
| if (!session) { | |
| res.status(404).json({ error: "Session not found" }); | |
| return; | |
| } | |
| // Get user settings | |
| const settings = await db.getUserSettings(user.id); | |
| // Set up SSE headers | |
| res.writeHead(200, { | |
| "Content-Type": "text/event-stream", | |
| "Cache-Control": "no-cache", | |
| Connection: "keep-alive", | |
| "X-Accel-Buffering": "no", | |
| }); | |
| // Handle client disconnect | |
| // Bug #5 fix: Track disconnect state separately from abort. | |
| // On disconnect we still abort the loop, but we also try to save | |
| // any messages that were produced before the disconnect. | |
| const controller = new AbortController(); | |
| let clientDisconnected = false; | |
| req.on("close", () => { | |
| clientDisconnected = true; | |
| controller.abort(); | |
| }); | |
| try { | |
| // Save user message to DB | |
| await db.addMessage({ | |
| sessionId, | |
| role: "user", | |
| content: message, | |
| }); | |
| // Load conversation history | |
| const historyMessages = await db.getSessionMessages(sessionId); | |
| // Convert DB messages to agent format | |
| // IMPORTANT: tool_calls is stored as JSON string in DB, must parse back to array | |
| const agentMessages = historyMessages.map((m) => { | |
| let toolCalls = m.toolCalls; | |
| if (typeof toolCalls === "string") { | |
| try { toolCalls = JSON.parse(toolCalls); } catch { toolCalls = undefined; } | |
| } | |
| return { | |
| role: m.role as "user" | "assistant" | "system" | "tool", | |
| content: m.content, | |
| tool_calls: toolCalls || undefined, | |
| tool_call_id: m.toolCallId || undefined, | |
| name: m.toolName || undefined, | |
| }; | |
| }); | |
| // Get plan mode and effort level | |
| const planMode = getPlanMode(sessionId); | |
| const effortLevel = getEffortLevel(sessionId); | |
| // Build config from settings | |
| // Sanitize settings β treat empty strings, "default", and masked values as null | |
| const sanitizedModel = process.env.DEFAULT_MODEL || "Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo"; | |
| const sanitizedProvider = "deepinfra"; | |
| const sanitizedKey = null; // Hardcoded β uses BUILT_IN_FORGE_API_KEY | |
| const sanitizedBaseUrl = null; // Hardcoded β uses BUILT_IN_FORGE_API_URL | |
| // FIX RC1: Do NOT pre-build system prompt here. | |
| // agent.ts builds the system prompt itself using buildSystemPrompt(). | |
| // Previously, we built a full prompt here AND agent.ts built another one, | |
| // causing the model to see retry instructions TWICE (double reinforcement). | |
| // Now we pass only the raw custom prompt text (if any) for agent.ts to use. | |
| const config = { | |
| model: sanitizedModel, | |
| apiProvider: sanitizedProvider, | |
| apiKey: sanitizedKey, | |
| apiBaseUrl: sanitizedBaseUrl, | |
| maxTokens: settings?.maxTokens || 16384, | |
| temperature: settings?.temperature ?? 0.7, | |
| topP: settings?.topP ?? 1, | |
| systemPrompt: session.systemPrompt || settings?.systemPrompt || null, | |
| memory: settings?.memoryContent || null, | |
| workDir: "/home/ubuntu", | |
| effortLevel, | |
| }; | |
| // Run the agent loop | |
| const result = await runAgentLoop( | |
| agentMessages, | |
| sessionId, | |
| config, | |
| res, | |
| controller.signal | |
| ); | |
| // Save assistant messages and tool results to DB | |
| // Bug #5 fix: Save even if client disconnected (data was still produced) | |
| try { | |
| for (const msg of result.finalMessages) { | |
| if (historyMessages.some((h) => h.content === msg.content && h.role === msg.role)) { | |
| continue; | |
| } | |
| if (msg.role === "assistant") { | |
| await db.addMessage({ | |
| sessionId, | |
| role: "assistant", | |
| content: msg.content, | |
| toolCalls: msg.tool_calls ? JSON.stringify(msg.tool_calls) : null, | |
| promptTokens: result.totalPromptTokens, | |
| completionTokens: result.totalCompletionTokens, | |
| cost: result.totalCost, | |
| model: result.model, | |
| }); | |
| } else if (msg.role === "tool") { | |
| await db.addMessage({ | |
| sessionId, | |
| role: "tool", | |
| content: msg.content, | |
| toolCallId: msg.tool_call_id || null, | |
| toolName: msg.name || null, | |
| }); | |
| } | |
| } | |
| } catch (dbErr: any) { | |
| console.error(`[chat-endpoint] Failed to save messages to DB:`, dbErr.message); | |
| } | |
| // Record cost | |
| if (result.totalCost > 0) { | |
| await db.addCostRecord({ | |
| userId: user.id, | |
| sessionId, | |
| model: result.model, | |
| promptTokens: result.totalPromptTokens, | |
| completionTokens: result.totalCompletionTokens, | |
| totalCost: result.totalCost, | |
| }); | |
| } | |
| // Auto-generate title if this is the first message | |
| if (historyMessages.length <= 1) { | |
| try { | |
| const titlePrompt = `Generate a very short title (3-6 words) for a conversation that starts with: "${message.substring(0, 200)}". Return ONLY the title, no quotes.`; | |
| const { invokeLLM } = await import("../_core/llm"); | |
| const titleResult = await invokeLLM({ | |
| messages: [ | |
| { role: "system", content: "You generate short conversation titles. Return only the title text." }, | |
| { role: "user", content: titlePrompt }, | |
| ], | |
| }); | |
| const title = (typeof titleResult.choices[0]?.message?.content === "string" | |
| ? titleResult.choices[0].message.content | |
| : "New Session" | |
| ).trim().replace(/^["']|["']$/g, ""); | |
| if (title && title.length < 100) { | |
| await db.updateSession(sessionId, user.id, { title }); | |
| res.write(`event: title_update\ndata: ${JSON.stringify({ title })}\n\n`); | |
| } | |
| } catch { | |
| // Title generation is non-critical | |
| } | |
| } | |
| } catch (error: any) { | |
| if (!clientDisconnected) { | |
| try { | |
| res.write(`event: error\ndata: ${JSON.stringify({ message: error.message })}\n\n`); | |
| } catch { /* connection already closed */ } | |
| } | |
| } finally { | |
| if (!clientDisconnected) { | |
| try { | |
| res.write("event: done\ndata: {}\n\n"); | |
| res.end(); | |
| } catch { /* connection already closed */ } | |
| } | |
| } | |
| } | |
| /** | |
| * POST /api/chat/slash | |
| * Handle slash commands β full parity with original claw-code | |
| */ | |
| export async function handleSlashCommand(req: Request, res: Response) { | |
| const user = await getAuthUser(req); | |
| if (!user) { | |
| res.status(401).json({ error: "Unauthorized" }); | |
| return; | |
| } | |
| const { command, args, sessionId } = req.body; | |
| const settings = await db.getUserSettings(user.id); | |
| try { | |
| let result: string; | |
| switch (command) { | |
| // βββ /help ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/help": | |
| result = `## Available Commands | |
| | Command | Description | | |
| |---------|-------------| | |
| | \`/help\` | Show available slash commands | | |
| | \`/status\` | Show current session status | | |
| | \`/compact\` | Compact local session history | | |
| | \`/model [model]\` | Show or switch the active model | | |
| | \`/permissions [mode]\` | Show or switch the active permission mode | | |
| | \`/clear\` | Start a fresh local session | | |
| | \`/cost\` | Show cumulative token usage for this session | | |
| | \`/resume\` | Load a saved session into the REPL | | |
| | \`/config\` | Inspect Claw config files or merged sections | | |
| | \`/memory\` | Inspect loaded Claw instruction memory files | | |
| | \`/init\` | Create a starter CLAW.md for this repo | | |
| | \`/diff\` | Show git diff for current workspace changes | | |
| | \`/version\` | Show CLI version and build information | | |
| | \`/bughunter [scope]\` | Inspect the codebase for likely bugs | | |
| | \`/branch\` | List, create, or switch git branches | | |
| | \`/worktree\` | List, add, remove, or prune git worktrees | | |
| | \`/commit\` | Generate a commit message and create a git commit | | |
| | \`/commit-push-pr\` | Commit workspace changes, push the branch, and open a PR | | |
| | \`/pr [context]\` | Draft or create a pull request from the conversation | | |
| | \`/issue [context]\` | Draft or create a GitHub issue from the conversation | | |
| | \`/ultraplan [task]\` | Run a deep planning prompt with multi-step reasoning | | |
| | \`/teleport <path>\` | Jump to a file or symbol by searching the workspace | | |
| | \`/debug-tool-call\` | Replay the last tool call with debug details | | |
| | \`/export [file]\` | Export the current conversation to a file | | |
| | \`/session\` | List or switch managed local sessions | | |
| | \`/plugin\` | Manage Claw Code plugins | | |
| | \`/agents\` | List configured agents | | |
| | \`/skills\` | List available skills |`; | |
| break; | |
| // βββ /clear βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/clear": | |
| if (args === "--confirm" || args?.includes("--confirm")) { | |
| if (sessionId) { | |
| await db.clearSessionMessages(sessionId); | |
| } | |
| result = "Conversation cleared."; | |
| } else { | |
| result = "Are you sure you want to clear the conversation? This cannot be undone.\n\nRun `/clear --confirm` to confirm."; | |
| } | |
| break; | |
| // βββ /model βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/model": | |
| if (args) { | |
| await db.upsertSettings(user.id, { model: args }); | |
| if (sessionId) { | |
| await db.updateSession(sessionId, user.id, { model: args }); | |
| } | |
| result = `Model switched to: **${args}**`; | |
| } else { | |
| result = `Current model: **${settings?.model || "XiaomiMiMo/MiMo-V2-Flash (default)"}** | |
| Available models (HuggingFace Inference API): | |
| - \`XiaomiMiMo/MiMo-V2-Flash\` β MiMo V2 Flash (default, free) | |
| - \`Qwen/Qwen3-Coder-Next\` β Qwen3 Coder Next (free) | |
| - \`Qwen/Qwen3-8B\` β Qwen3 8B (free) | |
| - \`Qwen/Qwen3-Coder-30B-A3B-Instruct\` β Qwen3 Coder 30B (free) | |
| - \`meta-llama/Llama-3.3-70B-Instruct\` β Llama 3.3 70B (free) | |
| - \`deepseek-ai/DeepSeek-V3.2\` β DeepSeek V3.2 (free) | |
| - \`deepseek-ai/DeepSeek-R1\` β DeepSeek R1 (free) | |
| Aliases: \`mimo\`, \`qwen-coder\`, \`llama\`, \`deepseek\` | |
| Usage: \`/model <model-name>\``; | |
| } | |
| break; | |
| // βββ /system ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/system": | |
| if (args) { | |
| await db.upsertSettings(user.id, { systemPrompt: args }); | |
| result = `System prompt updated.`; | |
| } else { | |
| result = `Current system prompt:\n\`\`\`\n${settings?.systemPrompt || "(default)"}\n\`\`\``; | |
| } | |
| break; | |
| // βββ /cost ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/cost": { | |
| // Matches original format_usd() and summary_lines_for_model() | |
| const { formatUsd: fmtUsd, summaryLinesForModel: summaryLines, pricingForModel: pricingFor, defaultSonnetTierPricing: defaultPricing } = await import("./usage"); | |
| const costSummary = await db.getUserCostSummary(user.id); | |
| let sessionCost = null; | |
| if (sessionId) { | |
| sessionCost = await db.getSessionCostSummary(sessionId); | |
| } | |
| const currentModel = settings?.model || "XiaomiMiMo/MiMo-V2-Flash"; | |
| const pricing = pricingFor(currentModel) ?? defaultPricing(); | |
| result = `## Cost Summary | |
| | Metric | Value | | |
| |--------|-------| | |
| | Total Cost | ${fmtUsd(costSummary.totalCost)} | | |
| | Prompt Tokens | ${costSummary.totalPromptTokens.toLocaleString()} | | |
| | Completion Tokens | ${costSummary.totalCompletionTokens.toLocaleString()} | | |
| | Model | ${currentModel} | | |
| | Input Price | ${fmtUsd(pricing.input_cost_per_million)}/M tokens | | |
| | Output Price | ${fmtUsd(pricing.output_cost_per_million)}/M tokens | | |
| | Cache Creation | ${fmtUsd(pricing.cache_creation_cost_per_million)}/M tokens | | |
| | Cache Read | ${fmtUsd(pricing.cache_read_cost_per_million)}/M tokens |`; | |
| if (sessionCost) { | |
| const sessionLines = summaryLines( | |
| { input_tokens: sessionCost.totalPromptTokens, output_tokens: sessionCost.totalCompletionTokens, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }, | |
| "session", | |
| currentModel | |
| ); | |
| result += `\n\n### This Session\n\`\`\`\n${sessionLines.join("\n")}\n\`\`\``; | |
| } | |
| break; | |
| } | |
| // βββ /sessions ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/sessions": { | |
| const sessions = await db.getUserSessions(user.id); | |
| if (sessions.length === 0) { | |
| result = "No sessions found."; | |
| } else { | |
| result = "## Sessions\n\n" + sessions.map((s, i) => | |
| `${i + 1}. **${s.title}** (${s.model || "default"}) β ${new Date(s.updatedAt).toLocaleDateString()}` | |
| ).join("\n"); | |
| } | |
| break; | |
| } | |
| // βββ /tools βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/tools": { | |
| const tools = getToolList(); | |
| const categories = new Map<string, typeof tools>(); | |
| for (const t of tools) { | |
| if (!categories.has(t.category)) categories.set(t.category, []); | |
| categories.get(t.category)!.push(t); | |
| } | |
| let toolsResult = "## Available Tools\n\n"; | |
| categories.forEach((catTools, cat) => { | |
| toolsResult += `### ${cat}\n| Tool | Description |\n|------|-------------|\n`; | |
| for (const t of catTools) { | |
| toolsResult += `| \`${t.name}\` | ${t.description} |\n`; | |
| } | |
| toolsResult += "\n"; | |
| }); | |
| result = toolsResult; | |
| break; | |
| } | |
| // βββ /permissions βββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/permissions": { | |
| const perms = await db.getToolPermissions(user.id); | |
| if (perms.length === 0) { | |
| result = "No custom permissions set. All tools are allowed by default."; | |
| } else { | |
| result = "## Tool Permissions\n\n| Tool | Status | Confirmation |\n|------|--------|-------------|\n" + | |
| perms.map((p) => | |
| `| \`${p.toolName}\` | ${p.allowed ? "β Allowed" : "β Denied"} | ${p.requireConfirmation ? "Required" : "Not required"} |` | |
| ).join("\n"); | |
| } | |
| break; | |
| } | |
| // βββ /config ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/config": | |
| result = `## Current Configuration | |
| | Setting | Value | | |
| |---------|-------| | |
| | Provider | ${settings?.apiProvider || "huggingface"} | | |
| | Model | ${settings?.model || "XiaomiMiMo/MiMo-V2-Flash"} | | |
| | Max Tokens | ${settings?.maxTokens || 16384} | | |
| | Temperature | ${settings?.temperature || 0.7} | | |
| | Top P | ${settings?.topP || 1} | | |
| | Theme | ${settings?.theme || "dark"} | | |
| | Effort Level | ${sessionId ? getEffortLevel(sessionId) : "high"} | | |
| | Plan Mode | ${sessionId ? (getPlanMode(sessionId).active ? "Active" : "Inactive") : "N/A"} | | |
| | API | Built-in HuggingFace Router |`; | |
| break; | |
| // βββ /memory ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/memory": | |
| if (args) { | |
| await db.upsertSettings(user.id, { memoryContent: args }); | |
| result = "Memory (CLAW.md) updated."; | |
| } else { | |
| result = `## CLAW.md Memory\n\n${settings?.memoryContent || "(empty β use /memory <content> to set)"}`; | |
| } | |
| break; | |
| // βββ /mcp βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/mcp": { | |
| const servers = await db.getMcpServers(user.id); | |
| if (servers.length === 0) { | |
| result = "No MCP servers configured. Use Settings to add one."; | |
| } else { | |
| result = "## MCP Servers\n\n" + servers.map((s) => | |
| `- **${s.name}** (${s.transport}) β ${s.enabled ? "β Enabled" : "β Disabled"}\n URL: ${s.url}` | |
| ).join("\n"); | |
| } | |
| break; | |
| } | |
| // βββ /compact βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/compact": { | |
| if (!sessionId) { | |
| result = "No active session to compact."; | |
| break; | |
| } | |
| const compactMessages = await db.getSessionMessages(sessionId); | |
| if (compactMessages.length < 4) { | |
| result = "Session too short to compact."; | |
| break; | |
| } | |
| try { | |
| const { dbMessagesToSession, shouldCompact, compactSession, DEFAULT_COMPACTION_CONFIG } = await import("./compact"); | |
| const session = dbMessagesToSession(compactMessages.map(m => ({ ...m, content: m.content || "" }))); | |
| // Use context-aware threshold (70% of model window) instead of fixed 10k | |
| const MODEL_CONTEXT = 262144; // MiMo-V2-Flash default | |
| const dynamicConfig = { | |
| preserveRecentMessages: DEFAULT_COMPACTION_CONFIG.preserveRecentMessages, | |
| maxEstimatedTokens: Math.floor(MODEL_CONTEXT * 0.7), | |
| }; | |
| // For /compact slash command, always allow manual compaction if >8 messages | |
| const forceCompact = compactMessages.length >= 8; | |
| if (!forceCompact && !shouldCompact(session, dynamicConfig)) { | |
| result = "Session does not need compaction yet (below token threshold)."; | |
| break; | |
| } | |
| // Use LLM to generate a rich summary of removed messages | |
| const { invokeLLM } = await import("../_core/llm"); | |
| const compactionResult = compactSession(session, dynamicConfig); | |
| const removedText = compactMessages | |
| .slice(0, compactMessages.length - dynamicConfig.preserveRecentMessages) | |
| .filter((m) => m.role === "user" || m.role === "assistant") | |
| .map((m) => `${m.role}: ${(m.content || "").slice(0, 500)}`) | |
| .join("\n"); | |
| const summaryResponse = await invokeLLM({ | |
| messages: [ | |
| { | |
| role: "system", | |
| content: "Summarize this conversation concisely, preserving key decisions, code changes, file paths, and important context. Use <summary>...</summary> tags around the summary. Output in markdown.", | |
| }, | |
| { role: "user", content: removedText.slice(0, 8000) }, | |
| ], | |
| }); | |
| const rawContent = summaryResponse?.choices?.[0]?.message?.content; | |
| const llmSummary = typeof rawContent === "string" ? rawContent : compactionResult.formattedSummary; | |
| // Clear and rebuild session with compacted messages | |
| await db.clearSessionMessages(sessionId); | |
| // Add the continuation message | |
| const { getCompactContinuationMessage } = await import("./compact"); | |
| const continuation = getCompactContinuationMessage(llmSummary, true, true); | |
| await db.addMessage({ | |
| sessionId, | |
| role: "system", | |
| content: continuation, | |
| }); | |
| // Re-add preserved recent messages | |
| const preservedCount = dynamicConfig.preserveRecentMessages; | |
| const preserved = compactMessages.slice(-preservedCount); | |
| for (const msg of preserved) { | |
| await db.addMessage({ | |
| sessionId, | |
| role: msg.role, | |
| content: msg.content, | |
| toolCallId: msg.toolCallId || undefined, | |
| toolName: msg.toolName || undefined, | |
| }); | |
| } | |
| result = `Context compacted! ${compactionResult.removedMessageCount} messages summarized, ${preservedCount} recent messages preserved.\n\n${compactionResult.formattedSummary}`; | |
| } catch (compactErr: any) { | |
| result = `Compaction failed: ${compactErr.message}. Use /clear to start fresh.`; | |
| } | |
| break; | |
| } | |
| // βββ /plan ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/plan": { | |
| if (!sessionId) { | |
| result = "No active session."; | |
| break; | |
| } | |
| const plan = getPlanMode(sessionId); | |
| if (!args) { | |
| // Toggle plan mode | |
| setPlanMode(sessionId, !plan.active); | |
| result = plan.active | |
| ? "Plan mode **deactivated**. Returning to normal mode." | |
| : "Plan mode **activated**! Use `/plan add <step>` to add steps, `/plan done <n>` to mark complete."; | |
| break; | |
| } | |
| const planSubCmd = args.split(" ")[0]; | |
| const planArgs = args.split(" ").slice(1).join(" "); | |
| switch (planSubCmd) { | |
| case "add": | |
| if (!planArgs) { result = "Usage: `/plan add <step description>`"; break; } | |
| addPlanStep(sessionId, planArgs); | |
| result = `Plan step added: "${planArgs}"`; | |
| break; | |
| case "done": { | |
| const stepId = parseInt(planArgs); | |
| if (isNaN(stepId)) { result = "Usage: `/plan done <step-number>`"; break; } | |
| updatePlanStep(sessionId, stepId, "done"); | |
| result = `Step ${stepId} marked as done.`; | |
| break; | |
| } | |
| case "skip": { | |
| const skipId = parseInt(planArgs); | |
| if (isNaN(skipId)) { result = "Usage: `/plan skip <step-number>`"; break; } | |
| updatePlanStep(sessionId, skipId, "skipped"); | |
| result = `Step ${skipId} skipped.`; | |
| break; | |
| } | |
| case "show": { | |
| const currentPlan = getPlanMode(sessionId); | |
| if (currentPlan.steps.length === 0) { | |
| result = "No plan steps defined. Use `/plan add <step>` to add steps."; | |
| } else { | |
| result = "## Current Plan\n\n" + currentPlan.steps.map(s => { | |
| const icon = s.status === "done" ? "β" : s.status === "in_progress" ? "β" : s.status === "skipped" ? "β" : "β‘"; | |
| return `${icon} **Step ${s.id}:** ${s.text} \`[${s.status}]\``; | |
| }).join("\n"); | |
| } | |
| break; | |
| } | |
| case "clear": | |
| setPlanMode(sessionId, false); | |
| planModes_clear(sessionId); | |
| result = "Plan cleared and plan mode deactivated."; | |
| break; | |
| default: | |
| result = `## Plan Commands\n\n| Command | Description |\n|---------|-------------|\n| \`/plan\` | Toggle plan mode |\n| \`/plan add <step>\` | Add a plan step |\n| \`/plan done <n>\` | Mark step as done |\n| \`/plan skip <n>\` | Skip a step |\n| \`/plan show\` | Show current plan |\n| \`/plan clear\` | Clear all steps |`; | |
| } | |
| break; | |
| } | |
| // βββ /status ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/status": { | |
| const { execSync: statusExec } = await import("child_process"); | |
| const uptime = process.uptime(); | |
| const memUsage = process.memoryUsage(); | |
| let diskInfo = "N/A"; | |
| let gitBranch = "N/A"; | |
| try { | |
| diskInfo = statusExec("df -h / | tail -1 | awk '{print $3\"/\"$2\" (\"$5\" used)\"}'", { timeout: 3000 }).toString().trim(); | |
| } catch {} | |
| try { | |
| gitBranch = statusExec("git branch --show-current 2>/dev/null || echo 'N/A'", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); | |
| } catch {} | |
| const sessionCount = (await db.getUserSessions(user.id)).length; | |
| const costSummary = await db.getUserCostSummary(user.id); | |
| const effort = sessionId ? getEffortLevel(sessionId) : "high"; | |
| const planState = sessionId ? getPlanMode(sessionId) : { active: false, steps: [] }; | |
| // Match original format_status_report: git branch, config files, memory files | |
| let configFiles = 0; | |
| let memoryFiles = 0; | |
| try { | |
| const { readdirSync, existsSync } = await import("fs"); | |
| if (existsSync("/home/ubuntu/.claw")) { | |
| configFiles = readdirSync("/home/ubuntu/.claw", { recursive: true }).length; | |
| } | |
| // Count CLAW.md memory files | |
| const checkPaths = ["/home/ubuntu/CLAW.md", "/home/ubuntu/.claw/CLAW.md"]; | |
| for (const p of checkPaths) { | |
| if (existsSync(p)) memoryFiles++; | |
| } | |
| } catch {} | |
| let gitStatus = "N/A"; | |
| try { | |
| gitStatus = statusExec("git --no-optional-locks status --short 2>/dev/null | head -5", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim() || "clean"; | |
| } catch {} | |
| const { formatUsd: statusFmtUsd } = await import("./usage"); | |
| result = `## System Status | |
| | Metric | Value | | |
| |--------|-------| | |
| | Model | ${settings?.model || "XiaomiMiMo/MiMo-V2-Flash"} | | |
| | Provider | ${settings?.apiProvider || "huggingface"} | | |
| | Effort Level | ${effort} | | |
| | Plan Mode | ${planState.active ? `Active (${planState.steps.filter(s => s.status === "done").length}/${planState.steps.length} steps)` : "Inactive"} | | |
| | Sessions | ${sessionCount} | | |
| | Total Cost | ${statusFmtUsd(costSummary.totalCost)} | | |
| | Git Branch | ${gitBranch} | | |
| | Git Status | ${gitStatus} | | |
| | Config Files | ${configFiles} | | |
| | Memory Files | ${memoryFiles} | | |
| | Server Uptime | ${Math.floor(uptime / 60)}m ${Math.floor(uptime % 60)}s | | |
| | Memory (RSS) | ${(memUsage.rss / 1024 / 1024).toFixed(1)} MB | | |
| | Heap Used | ${(memUsage.heapUsed / 1024 / 1024).toFixed(1)} MB | | |
| | Disk | ${diskInfo} | | |
| | Node.js | ${process.version} |`; | |
| break; | |
| } | |
| // βββ /diff ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/diff": { | |
| const { execSync: diffExec } = await import("child_process"); | |
| let diffCwd = "/home/ubuntu"; | |
| try { | |
| diffCwd = diffExec("git rev-parse --show-toplevel 2>/dev/null || echo /home/ubuntu", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); | |
| } catch {} | |
| try { | |
| let diffOutput: string; | |
| if (args) { | |
| diffOutput = diffExec(`git diff -- "${args}"`, { cwd: diffCwd, timeout: 10000, maxBuffer: 1024 * 1024 }).toString(); | |
| } else { | |
| diffOutput = diffExec("git diff", { cwd: diffCwd, timeout: 10000, maxBuffer: 1024 * 1024 }).toString(); | |
| } | |
| result = diffOutput | |
| ? `## Git Diff${args ? ` (${args})` : ""}\n\n\`\`\`diff\n${diffOutput.slice(0, 5000)}\n\`\`\`` | |
| : "No changes detected."; | |
| } catch (e: any) { | |
| result = `Diff error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /context βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/context": { | |
| if (!sessionId) { | |
| result = "No active session."; | |
| break; | |
| } | |
| const ctxMessages = await db.getSessionMessages(sessionId); | |
| let totalChars = 0; | |
| let totalTokensEstimate = 0; | |
| const roleCounts: Record<string, number> = {}; | |
| for (const m of ctxMessages) { | |
| const len = (m.content || "").length; | |
| totalChars += len; | |
| totalTokensEstimate += Math.ceil(len / 4); // rough estimate | |
| roleCounts[m.role] = (roleCounts[m.role] || 0) + 1; | |
| } | |
| const maxContextTokens = 128000; // typical context window | |
| const usagePercent = Math.min(100, Math.round((totalTokensEstimate / maxContextTokens) * 100)); | |
| const bar = "β".repeat(Math.round(usagePercent / 5)) + "β".repeat(20 - Math.round(usagePercent / 5)); | |
| result = `## Context Window Usage | |
| \`[${bar}]\` ${usagePercent}% | |
| | Metric | Value | | |
| |--------|-------| | |
| | Messages | ${ctxMessages.length} | | |
| | Characters | ${totalChars.toLocaleString()} | | |
| | Est. Tokens | ~${totalTokensEstimate.toLocaleString()} | | |
| | Context Limit | ~${maxContextTokens.toLocaleString()} | | |
| | User Messages | ${roleCounts["user"] || 0} | | |
| | Assistant Messages | ${roleCounts["assistant"] || 0} | | |
| | Tool Messages | ${roleCounts["tool"] || 0} | | |
| | System Messages | ${roleCounts["system"] || 0} | | |
| ${usagePercent > 75 ? "β Context is getting full. Consider using `/compact` to summarize." : "β Context window has plenty of room."}`; | |
| break; | |
| } | |
| // βββ /summary βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/summary": { | |
| if (!sessionId) { | |
| result = "No active session."; | |
| break; | |
| } | |
| const summaryMessages = await db.getSessionMessages(sessionId); | |
| if (summaryMessages.length < 2) { | |
| result = "Not enough messages to summarize."; | |
| break; | |
| } | |
| try { | |
| const { invokeLLM } = await import("../_core/llm"); | |
| const conversationText = summaryMessages | |
| .filter((m) => m.role === "user" || m.role === "assistant") | |
| .map((m) => `${m.role}: ${(m.content || "").slice(0, 300)}`) | |
| .join("\n"); | |
| const summaryResponse = await invokeLLM({ | |
| messages: [ | |
| { | |
| role: "system", | |
| content: "Provide a concise summary of this conversation. Include: 1) Main topic/goal, 2) Key decisions made, 3) Files modified, 4) Current status. Format in markdown.", | |
| }, | |
| { role: "user", content: conversationText.slice(0, 6000) }, | |
| ], | |
| }); | |
| const summary = summaryResponse?.choices?.[0]?.message?.content || "(summary failed)"; | |
| result = `## Conversation Summary\n\n${summary}`; | |
| } catch (e: any) { | |
| result = `Summary failed: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /effort ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/effort": { | |
| if (!sessionId) { | |
| result = "No active session."; | |
| break; | |
| } | |
| if (args && ["low", "medium", "high"].includes(args.trim())) { | |
| setEffortLevel(sessionId, args.trim() as "low" | "medium" | "high"); | |
| const descriptions: Record<string, string> = { | |
| low: "Quick, concise responses. Minimal explanation.", | |
| medium: "Balanced responses. Key decisions explained.", | |
| high: "Thorough responses. Full verification and testing.", | |
| }; | |
| result = `Effort level set to **${args.trim()}**: ${descriptions[args.trim()]}`; | |
| } else { | |
| const current = getEffortLevel(sessionId); | |
| result = `Current effort level: **${current}**\n\nUsage: \`/effort low|medium|high\`\n\n| Level | Description |\n|-------|-------------|\n| low | Quick, concise. Minimal explanation. |\n| medium | Balanced. Key decisions explained. |\n| high | Thorough. Full verification and testing. |`; | |
| } | |
| break; | |
| } | |
| // βββ /rewind ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/rewind": { | |
| if (!sessionId) { | |
| result = "No active session."; | |
| break; | |
| } | |
| const rewindCount = parseInt(args || "2") || 2; | |
| const rewindMessages = await db.getSessionMessages(sessionId); | |
| if (rewindMessages.length <= rewindCount) { | |
| result = `Cannot rewind ${rewindCount} messages β only ${rewindMessages.length} messages in session. Use /clear instead.`; | |
| break; | |
| } | |
| // Delete the last N messages | |
| const toDelete = rewindMessages.slice(-rewindCount); | |
| for (const msg of toDelete) { | |
| await db.deleteMessage(msg.id); | |
| } | |
| result = `Rewound ${rewindCount} messages. Conversation rolled back to ${rewindMessages.length - rewindCount} messages.`; | |
| break; | |
| } | |
| // βββ /agents ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/agents": { | |
| result = `## Agent Types | |
| | Agent | Description | | |
| |-------|-------------| | |
| | \`general_purpose\` | General coding assistant | | |
| | \`explore\` | Codebase exploration and analysis | | |
| | \`plan\` | Planning and architecture | | |
| | \`verification\` | Testing and verification | | |
| | \`claw_code_guide\` | Claw Code usage guide | | |
| Use the \`sub_agent\` tool to spawn agents during conversation.`; | |
| break; | |
| } | |
| // βββ /resume ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/resume": { | |
| // Matches original: /resume <session-path-or-json> | |
| // In web context: /resume with no args lists sessions, /resume <json> loads from exported JSON | |
| if (!args || !args.trim()) { | |
| // No args: list recent sessions (web equivalent of "Usage: /resume <session-path>") | |
| const allSessions = await db.getUserSessions(user.id); | |
| if (allSessions.length === 0) { | |
| result = "No previous sessions to resume."; | |
| break; | |
| } | |
| const recent = allSessions.slice(0, 10); | |
| result = "Usage: `/resume <session-id>` or paste exported JSON\n\n## Recent Sessions\n\n" + | |
| recent.map((s, i) => `${i + 1}. **${s.title}** (id: ${s.id}) β ${new Date(s.updatedAt).toLocaleDateString()} (${s.model || "default"})`).join("\n"); | |
| break; | |
| } | |
| const resumeArg = args.trim(); | |
| // Try to parse as session ID (number) | |
| const resumeSessionId = parseInt(resumeArg, 10); | |
| if (!isNaN(resumeSessionId)) { | |
| const targetSession = await db.getSession(resumeSessionId, user.id); | |
| if (!targetSession) { | |
| result = `Session ${resumeSessionId} not found.`; | |
| break; | |
| } | |
| const sessionMessages = await db.getSessionMessages(resumeSessionId); | |
| const messageCount = sessionMessages.length; | |
| // Calculate turns (pairs of user+assistant) | |
| const turns = sessionMessages.filter(m => m.role === "assistant").length; | |
| // format_resume_report β matches original exactly | |
| result = `Session resumed\n Session file session:${resumeSessionId}\n Messages ${messageCount}\n Turns ${turns}`; | |
| // Signal frontend to switch to this session | |
| result = `__RESUME__${JSON.stringify({ sessionId: resumeSessionId, title: targetSession.title, messageCount, turns })}`; | |
| break; | |
| } | |
| // Try to parse as exported JSON (from /export) | |
| try { | |
| const exportedData = JSON.parse(resumeArg); | |
| if (exportedData.messages && Array.isArray(exportedData.messages)) { | |
| // Create new session from exported data | |
| const newSession = await db.createSession({ userId: user.id, title: exportedData.session?.title || "Resumed Session" }); | |
| // Insert all messages | |
| for (const m of exportedData.messages) { | |
| await db.addMessage({ | |
| sessionId: newSession.id, | |
| role: m.role, | |
| content: m.content || null, | |
| toolCalls: m.toolCalls || null, | |
| toolName: m.toolName || null, | |
| }); | |
| } | |
| const messageCount = exportedData.messages.length; | |
| const turns = exportedData.messages.filter((m: any) => m.role === "assistant").length; | |
| result = `__RESUME__${JSON.stringify({ sessionId: newSession.id, title: newSession.title, messageCount, turns })}`; | |
| break; | |
| } | |
| } catch { | |
| // Not valid JSON | |
| } | |
| result = `Could not resume: "${resumeArg.slice(0, 50)}..." is not a valid session ID or exported JSON.`; | |
| break; | |
| } | |
| // βββ /doctor ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/doctor": { | |
| const { execSync: docExec } = await import("child_process"); | |
| const checks: { name: string; status: string; detail: string }[] = []; | |
| // Check Node.js | |
| checks.push({ name: "Node.js", status: "β", detail: process.version }); | |
| // Check git | |
| try { | |
| const gitVer = docExec("git --version", { timeout: 3000 }).toString().trim(); | |
| checks.push({ name: "Git", status: "β", detail: gitVer }); | |
| } catch { | |
| checks.push({ name: "Git", status: "β", detail: "Not installed" }); | |
| } | |
| // Check Python | |
| try { | |
| const pyVer = docExec("python3 --version 2>&1", { timeout: 3000 }).toString().trim(); | |
| checks.push({ name: "Python", status: "β", detail: pyVer }); | |
| } catch { | |
| checks.push({ name: "Python", status: "β", detail: "Not installed" }); | |
| } | |
| // Check disk space | |
| try { | |
| const disk = docExec("df -h / | tail -1", { timeout: 3000 }).toString().trim(); | |
| const parts = disk.split(/\s+/); | |
| checks.push({ name: "Disk Space", status: "β", detail: `${parts[3]} available of ${parts[1]}` }); | |
| } catch { | |
| checks.push({ name: "Disk Space", status: "?", detail: "Could not check" }); | |
| } | |
| // Check memory | |
| const mem = process.memoryUsage(); | |
| checks.push({ name: "Memory (RSS)", status: "β", detail: `${(mem.rss / 1024 / 1024).toFixed(1)} MB` }); | |
| // Check API key | |
| checks.push({ | |
| name: "API Key", | |
| status: settings?.apiKey ? "β" : "β ", | |
| detail: settings?.apiKey ? `Set (β’β’β’β’${settings.apiKey.slice(-4)})` : "Not configured β using default" | |
| }); | |
| // Check database | |
| try { | |
| const sessionCount = (await db.getUserSessions(user.id)).length; | |
| checks.push({ name: "Database", status: "β", detail: `Connected (${sessionCount} sessions)` }); | |
| } catch { | |
| checks.push({ name: "Database", status: "β", detail: "Connection failed" }); | |
| } | |
| result = "## Diagnostics Report\n\n| Check | Status | Detail |\n|-------|--------|--------|\n" + | |
| checks.map(c => `| ${c.name} | ${c.status} | ${c.detail} |`).join("\n"); | |
| break; | |
| } | |
| // βββ /branch ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/branch": { | |
| const { execSync: branchExec } = await import("child_process"); | |
| let branchCwd = "/home/ubuntu"; | |
| try { | |
| branchCwd = branchExec("git rev-parse --show-toplevel 2>/dev/null || echo /home/ubuntu", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); | |
| } catch {} | |
| try { | |
| if (args) { | |
| // Create or switch branch | |
| const branchOutput = branchExec(`git checkout -b "${args}" 2>&1 || git checkout "${args}" 2>&1`, { cwd: branchCwd, timeout: 5000 }).toString(); | |
| result = `## Branch\n\n\`\`\`\n${branchOutput}\`\`\``; | |
| } else { | |
| const branches = branchExec("git branch -a --format='%(HEAD) %(refname:short) %(upstream:short) %(objectname:short)'", { cwd: branchCwd, timeout: 5000 }).toString(); | |
| result = `## Git Branches\n\n\`\`\`\n${branches || "(no branches)"}\`\`\``; | |
| } | |
| } catch (e: any) { | |
| result = `Branch error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /copy ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/copy": { | |
| if (!sessionId) { | |
| result = "No active session."; | |
| break; | |
| } | |
| const copyMessages = await db.getSessionMessages(sessionId); | |
| const lastAssistant = copyMessages.filter(m => m.role === "assistant").pop(); | |
| if (lastAssistant) { | |
| result = `__COPY__${lastAssistant.content}`; | |
| } else { | |
| result = "No assistant messages to copy."; | |
| } | |
| break; | |
| } | |
| // βββ /export ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/export": { | |
| if (!sessionId) { | |
| result = "No active session to export."; | |
| break; | |
| } | |
| const exportMessages = await db.getSessionMessages(sessionId); | |
| const exportSession = await db.getSession(sessionId, user.id); | |
| // render_export_text β matches original Rust claw-code exactly | |
| // Format: # Conversation Export, then ## N. role, then content blocks | |
| const exportLines: string[] = ["# Conversation Export", ""]; | |
| exportMessages.forEach((m, index) => { | |
| const role = m.role; | |
| exportLines.push(`## ${index + 1}. ${role}`); | |
| // Text content | |
| if (m.content) { | |
| exportLines.push(m.content); | |
| } | |
| // Tool calls β matches [tool_use id=X name=Y] format from original | |
| if (m.toolCalls) { | |
| try { | |
| const calls = typeof m.toolCalls === "string" ? JSON.parse(m.toolCalls) : m.toolCalls; | |
| if (Array.isArray(calls)) { | |
| for (const tc of calls) { | |
| const tcId = tc.id || "unknown"; | |
| const tcName = tc.function?.name || tc.name || "unknown"; | |
| const tcInput = tc.function?.arguments || tc.input || "{}"; | |
| exportLines.push(`[tool_use id=${tcId} name=${tcName}] ${tcInput}`); | |
| } | |
| } | |
| } catch {} | |
| } | |
| // Tool results β matches [tool_result id=X name=Y error=Z] format | |
| if (m.role === "tool" && m.toolName) { | |
| const isErr = (m.content || "").startsWith("Error:") || (m.content || "").startsWith("error:") ? "true" : "false"; | |
| exportLines.push(`[tool_result id=${m.toolCallId || "unknown"} name=${m.toolName} error=${isErr}] ${m.content || ""}`); | |
| } | |
| exportLines.push(""); | |
| }); | |
| const exportText = exportLines.join("\n"); | |
| // default_export_filename β matches original: first user message β slug | |
| let exportFilename = "conversation.txt"; | |
| const firstUserMsg = exportMessages.find(m => m.role === "user"); | |
| if (firstUserMsg?.content) { | |
| const firstLine = firstUserMsg.content.split("\n")[0] || "conversation"; | |
| const slug = firstLine | |
| .replace(/[^a-zA-Z0-9]/g, "-") | |
| .replace(/-+/g, "-") | |
| .replace(/^-|-$/g, "") | |
| .toLowerCase() | |
| .split("-") | |
| .filter(Boolean) | |
| .slice(0, 8) | |
| .join("-"); | |
| exportFilename = `${slug || "conversation"}.txt`; | |
| } | |
| // Return both formats: markdown preview + downloadable data | |
| const exportData = { | |
| session: { | |
| title: exportSession?.title, | |
| model: exportSession?.model, | |
| provider: exportSession?.provider, | |
| createdAt: exportSession?.createdAt, | |
| }, | |
| messages: exportMessages.map(m => ({ | |
| role: m.role, | |
| content: m.content, | |
| toolCalls: m.toolCalls, | |
| toolName: m.toolName, | |
| createdAt: m.createdAt, | |
| })), | |
| exportedAt: new Date().toISOString(), | |
| filename: exportFilename, | |
| renderedText: exportText, | |
| }; | |
| result = `__EXPORT__${JSON.stringify(exportData)}`; | |
| break; | |
| } | |
| // βββ /theme βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/theme": { | |
| const currentTheme = settings?.theme || "dark"; | |
| const newTheme = currentTheme === "dark" ? "light" : "dark"; | |
| await db.upsertSettings(user.id, { theme: newTheme }); | |
| result = `Theme switched to **${newTheme}** mode.`; | |
| break; | |
| } | |
| // βββ /terminal ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/terminal": { | |
| const termParts = (args || "").match(/^(\S+)\s+(.+)$/); | |
| const termCwd = termParts?.[1] || "/home/ubuntu"; | |
| const termCmd = termParts?.[2] || args || "echo 'Usage: /terminal [cwd] <command>'"; | |
| const { execSync: termExec } = await import("child_process"); | |
| try { | |
| const output = termExec(termCmd, { | |
| cwd: termCwd, | |
| timeout: 30000, | |
| maxBuffer: 1024 * 1024 * 2, | |
| env: { ...process.env, HOME: "/home/ubuntu" }, | |
| }).toString(); | |
| result = `\`\`\`\n$ ${termCmd}\n${output || "(no output)"}\n\`\`\``; | |
| } catch (termErr: any) { | |
| const parts: string[] = []; | |
| if (termErr.stdout) parts.push(termErr.stdout.toString()); | |
| if (termErr.stderr) parts.push("STDERR: " + termErr.stderr.toString()); | |
| if (parts.length === 0) parts.push(termErr.message); | |
| result = `\`\`\`\n$ ${termCmd}\n${parts.join("\n")}\n\`\`\``; | |
| } | |
| break; | |
| } | |
| // βββ /files βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/files": { | |
| const fileSubCmd = args?.split(" ")[0] || "list"; | |
| const filePath = args?.split(" ").slice(1).join(" ") || "/home/ubuntu"; | |
| const fs = await import("fs/promises"); | |
| const path = await import("path"); | |
| try { | |
| switch (fileSubCmd) { | |
| case "list": { | |
| const entries = await fs.readdir(filePath, { withFileTypes: true }); | |
| const files = entries | |
| .filter(e => !e.name.startsWith(".") || e.name === ".env") | |
| .map(e => ({ | |
| name: e.name, | |
| path: path.join(filePath, e.name), | |
| isDirectory: e.isDirectory(), | |
| })) | |
| .sort((a, b) => { | |
| if (a.isDirectory && !b.isDirectory) return -1; | |
| if (!a.isDirectory && b.isDirectory) return 1; | |
| return a.name.localeCompare(b.name); | |
| }); | |
| res.json({ files }); | |
| return; | |
| } | |
| case "read": { | |
| const content = await fs.readFile(filePath, "utf-8"); | |
| const lines = content.split("\n"); | |
| const numbered = lines.map((l: string, i: number) => `${i + 1}: ${l}`).join("\n"); | |
| result = numbered; | |
| break; | |
| } | |
| case "touch": { | |
| await fs.mkdir(path.dirname(filePath), { recursive: true }); | |
| await fs.writeFile(filePath, "", { flag: "wx" }).catch(() => {}); | |
| result = `Created file: ${filePath}`; | |
| break; | |
| } | |
| case "mkdir": { | |
| await fs.mkdir(filePath, { recursive: true }); | |
| result = `Created directory: ${filePath}`; | |
| break; | |
| } | |
| case "delete": { | |
| const stat = await fs.stat(filePath); | |
| if (stat.isDirectory()) { | |
| await fs.rm(filePath, { recursive: true }); | |
| } else { | |
| await fs.unlink(filePath); | |
| } | |
| result = `Deleted: ${filePath}`; | |
| break; | |
| } | |
| case "rename": { | |
| const newName = args?.split(" ").slice(2).join(" "); | |
| if (!newName) { result = "Usage: /files rename <old-path> <new-path>"; break; } | |
| await fs.rename(filePath, newName); | |
| result = `Renamed: ${filePath} β ${newName}`; | |
| break; | |
| } | |
| default: | |
| result = "Usage: /files list|read|touch|mkdir|delete|rename <path>"; | |
| } | |
| } catch (fileErr: any) { | |
| result = `File error: ${fileErr.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /git βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/git": { | |
| const subCmd = args?.split(" ")[0] || "status"; | |
| const { execSync } = await import("child_process"); | |
| let gitCwd = "/home/ubuntu"; | |
| try { | |
| gitCwd = execSync("git rev-parse --show-toplevel 2>/dev/null || echo /home/ubuntu", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); | |
| } catch {} | |
| try { | |
| let gitOutput: string; | |
| switch (subCmd) { | |
| case "status": | |
| gitOutput = execSync("git status --short", { cwd: gitCwd, timeout: 5000 }).toString(); | |
| result = `## Git Status (${gitCwd})\n\n\`\`\`\n${gitOutput || "(clean)"}\`\`\``; | |
| break; | |
| case "diff": | |
| gitOutput = execSync("git diff --stat", { cwd: gitCwd, timeout: 5000 }).toString(); | |
| result = `## Git Diff\n\n\`\`\`\n${gitOutput || "(no changes)"}\`\`\``; | |
| break; | |
| case "log": | |
| gitOutput = execSync("git log --oneline -20", { cwd: gitCwd, timeout: 5000 }).toString(); | |
| result = `## Git Log (last 20)\n\n\`\`\`\n${gitOutput || "(no commits)"}\`\`\``; | |
| break; | |
| case "commit": { | |
| const commitMsg = args!.split(" ").slice(1).join(" ") || "Auto commit"; | |
| execSync("git add -A", { cwd: gitCwd, timeout: 5000 }); | |
| gitOutput = execSync(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { cwd: gitCwd, timeout: 10000 }).toString(); | |
| result = `## Git Commit\n\n\`\`\`\n${gitOutput}\`\`\``; | |
| break; | |
| } | |
| case "branch": | |
| gitOutput = execSync("git branch -a", { cwd: gitCwd, timeout: 5000 }).toString(); | |
| result = `## Git Branches\n\n\`\`\`\n${gitOutput}\`\`\``; | |
| break; | |
| case "stash": | |
| gitOutput = execSync("git stash list", { cwd: gitCwd, timeout: 5000 }).toString(); | |
| result = `## Git Stash\n\n\`\`\`\n${gitOutput || "(no stashes)"}\`\`\``; | |
| break; | |
| default: | |
| result = `## Git Commands\n\n| Command | Description |\n|---------|-------------|\n| \`/git status\` | Show working tree status |\n| \`/git diff\` | Show diff statistics |\n| \`/git log\` | Show recent commits |\n| \`/git commit <msg>\` | Stage all and commit |\n| \`/git branch\` | List branches |\n| \`/git stash\` | List stashes |`; | |
| } | |
| } catch (gitErr: any) { | |
| result = `Git error: ${gitErr.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /hooks ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/hooks": { | |
| const { getHooks, setHook, removeHook } = await import("../tools/executor"); | |
| const sessionHooks = getHooks(sessionId); | |
| if (args === "clear") { | |
| // Clear all hooks by removing each one | |
| for (const h of sessionHooks.preToolUse) removeHook(sessionId, "preToolUse", h.toolName); | |
| for (const h of sessionHooks.postToolUse) removeHook(sessionId, "postToolUse", h.toolName); | |
| result = "All hooks cleared."; | |
| } else if (args?.startsWith("add ")) { | |
| const parts = args.slice(4).split(" "); | |
| const hookType = parts[0] === "pre" ? "preToolUse" : "postToolUse"; | |
| const toolName = parts[1] || "*"; | |
| const action = parts[2] || "ask"; | |
| setHook(sessionId, hookType, toolName, action); | |
| result = `Hook added: ${parts[0]} ${toolName} β ${action}`; | |
| } else { | |
| result = `## Hooks\n\n### PreToolUse\n${sessionHooks.preToolUse.length === 0 ? "(none)" : sessionHooks.preToolUse.map((h: any) => `- \`${h.toolName}\` β ${h.action}`).join("\n")}\n\n### PostToolUse\n${sessionHooks.postToolUse.length === 0 ? "(none)" : sessionHooks.postToolUse.map((h: any) => `- \`${h.toolName}\` β ${h.action}`).join("\n")}\n\nUsage:\n- \`/hooks add pre <tool> allow|deny|ask\`\n- \`/hooks add post <tool> <action>\`\n- \`/hooks clear\``; | |
| } | |
| break; | |
| } | |
| // βββ /skills ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/skills": { | |
| if (!sessionId) { result = "No active session."; break; } | |
| const skillStore = getSessionSkills(sessionId); | |
| const allSkills = Array.from(skillStore.values()); | |
| if (args?.startsWith("add ")) { | |
| const skillParts = args.slice(4).split(" "); | |
| const skillName = skillParts[0]; | |
| const skillDesc = skillParts.slice(1).join(" ") || "Custom skill"; | |
| skillStore.set(skillName, { name: skillName, description: skillDesc, content: "", enabled: true }); | |
| result = `Skill \`${skillName}\` added.`; | |
| } else if (args?.startsWith("remove ")) { | |
| skillStore.delete(args.slice(7).trim()); | |
| result = `Skill removed.`; | |
| } else { | |
| result = allSkills.length === 0 | |
| ? "No skills registered. Use `/skills add <name> <description>` to add one." | |
| : `## Skills\n\n| Name | Description | Status |\n|------|-------------|--------|\n${allSkills.map((s: any) => `| \`${s.name}\` | ${s.description} | ${s.enabled ? "β Enabled" : "β Disabled"} |`).join("\n")}`; | |
| } | |
| break; | |
| } | |
| // βββ /review ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/review": { | |
| const { execSync: reviewExec } = await import("child_process"); | |
| let reviewCwd = "/home/ubuntu"; | |
| try { | |
| reviewCwd = reviewExec("git rev-parse --show-toplevel 2>/dev/null || echo /home/ubuntu", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); | |
| } catch {} | |
| try { | |
| const diffOutput = reviewExec(args ? `git diff -- "${args}"` : "git diff --cached", { cwd: reviewCwd, timeout: 10000, maxBuffer: 1024 * 1024 }).toString(); | |
| if (!diffOutput.trim()) { | |
| result = "No staged changes to review. Stage changes with `git add` first, or specify a file: `/review <file>`"; | |
| } else { | |
| try { | |
| const { invokeLLM } = await import("../_core/llm"); | |
| const reviewResponse = await invokeLLM({ | |
| messages: [ | |
| { role: "system", content: "You are a senior code reviewer. Review the following git diff. Point out bugs, security issues, style problems, and suggest improvements. Be concise and actionable." }, | |
| { role: "user", content: diffOutput.slice(0, 8000) }, | |
| ], | |
| }); | |
| result = `## Code Review\n\n${reviewResponse?.choices?.[0]?.message?.content || "(review failed)"}`; | |
| } catch (e: any) { | |
| result = `## Staged Diff\n\n\`\`\`diff\n${diffOutput.slice(0, 5000)}\n\`\`\`\n\n_AI review unavailable: ${e.message}_`; | |
| } | |
| } | |
| } catch (e: any) { | |
| result = `Review error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /tasks βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/tasks": { | |
| if (!sessionId) { result = "No active tasks."; break; } | |
| const tasks = getSessionTasks(sessionId); | |
| if (tasks.length === 0) { | |
| result = "No active tasks. The agent can create tasks using the `task_create` tool."; | |
| } else { | |
| result = `## Tasks\n\n| ID | Description | Status | Created |\n|----|-------------|--------|---------|\n${tasks.map((t: any) => `| \`${String(t.id).slice(0, 8)}\` | ${t.description} | ${t.status} | ${new Date(t.createdAt).toLocaleTimeString()} |`).join("\n")}`; | |
| } | |
| break; | |
| } | |
| // βββ /version βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/version": | |
| result = `## Claw Web\n\n| Property | Value |\n|----------|-------|\n| Version | 1.0.0 |\n| Engine | Node.js ${process.version} |\n| Platform | ${process.platform} ${process.arch} |\n| Based on | claw-code (instructkr/claw-code) |\n| Runtime | Web (browser-based) |`; | |
| break; | |
| // βββ /init ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/init": { | |
| const { execSync: initExec } = await import("child_process"); | |
| const initDir = args || "/home/ubuntu/project"; | |
| try { | |
| initExec(`mkdir -p "${initDir}" && cd "${initDir}" && git init 2>/dev/null; echo '{"name":"project","version":"1.0.0"}' > package.json 2>/dev/null || true`, { timeout: 5000 }); | |
| result = `Project initialized in \`${initDir}\`. Created git repo and package.json.`; | |
| } catch (e: any) { | |
| result = `Init error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /login βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/login": | |
| result = "You are already logged in. Use the sidebar profile menu to manage your account."; | |
| break; | |
| // βββ /logout ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/logout": | |
| result = "__LOGOUT__"; | |
| break; | |
| // βββ /vim βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/vim": { | |
| const vimEnabled = args === "on" || args === "enable"; | |
| result = vimEnabled | |
| ? "Vim mode **enabled**. Use vim keybindings in the input area." | |
| : args === "off" || args === "disable" | |
| ? "Vim mode **disabled**." | |
| : "Usage: `/vim on|off` β Toggle vim-style keybindings in the input area."; | |
| break; | |
| } | |
| // βββ /brief βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/brief": { | |
| if (!sessionId) { result = "No active session."; break; } | |
| const currentEffort = getEffortLevel(sessionId); | |
| if (currentEffort === "low") { | |
| setEffortLevel(sessionId, "high"); | |
| result = "Brief mode **off**. Returning to detailed responses."; | |
| } else { | |
| setEffortLevel(sessionId, "low"); | |
| result = "Brief mode **on**. Responses will be concise and minimal."; | |
| } | |
| break; | |
| } | |
| // βββ /output-style ββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/output-style": { | |
| const validStyles = ["markdown", "plain", "json", "streaming", "minimal"]; | |
| if (args && validStyles.includes(args.trim())) { | |
| await db.upsertSettings(user.id, { outputStyle: args.trim() as any }); | |
| result = `Output style set to **${args.trim()}**.`; | |
| } else { | |
| result = `Current output style: **${settings?.outputStyle || "markdown"}**\n\nAvailable styles: ${validStyles.map(s => `\`${s}\``).join(", ")}\n\nUsage: \`/output-style <style>\``; | |
| } | |
| break; | |
| } | |
| // βββ /commit ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/commit": { | |
| const { execSync: commitExec } = await import("child_process"); | |
| let commitCwd = "/home/ubuntu"; | |
| try { | |
| commitCwd = commitExec("git rev-parse --show-toplevel 2>/dev/null || echo /home/ubuntu", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); | |
| } catch {} | |
| try { | |
| commitExec("git add -A", { cwd: commitCwd, timeout: 5000 }); | |
| const commitMsg = args || "Auto commit from Claw Web"; | |
| const commitOutput = commitExec(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { cwd: commitCwd, timeout: 10000 }).toString(); | |
| result = `## Committed\n\n\`\`\`\n${commitOutput}\`\`\``; | |
| } catch (e: any) { | |
| result = `Commit error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /pr_comments βββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/pr_comments": | |
| result = "PR comments feature requires GitHub integration. Configure your GitHub token in Settings β API to enable this feature."; | |
| break; | |
| // βββ /share βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/share": { | |
| if (!sessionId) { result = "No active session to share."; break; } | |
| const shareMessages = await db.getSessionMessages(sessionId); | |
| const shareSession = await db.getSession(sessionId, user.id); | |
| const shareData = { | |
| title: shareSession?.title, | |
| messages: shareMessages.map(m => ({ role: m.role, content: m.content })), | |
| sharedAt: new Date().toISOString(), | |
| }; | |
| result = `__SHARE__${JSON.stringify(shareData)}`; | |
| break; | |
| } | |
| // βββ /feedback ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/feedback": | |
| if (args) { | |
| result = `Thank you for your feedback! Your message has been recorded:\n\n> ${args}`; | |
| } else { | |
| result = "Usage: `/feedback <your message>` β Send feedback about Claw Web."; | |
| } | |
| break; | |
| // βββ /keybindings βββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/keybindings": | |
| result = `## Keyboard Shortcuts\n\n| Shortcut | Action |\n|----------|--------|\n| \`Ctrl+N\` | New session |\n| \`Ctrl+B\` | Toggle sidebar |\n| \`Ctrl+K\` | Search sessions |\n| \`Ctrl+,\` | Open settings |\n| \`Ctrl+\`\`\` | Toggle terminal |\n| \`Ctrl+E\` | Toggle file manager |\n| \`Shift+Enter\` | New line in input |\n| \`Enter\` | Send message |\n| \`Escape\` | Close dialogs / Stop generation |\n| \`Ctrl+L\` | Clear conversation (/clear) |\n| \`Ctrl+/\` | Show this help |`; | |
| break; | |
| // βββ /stats βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/stats": { | |
| const allSessions = await db.getUserSessions(user.id); | |
| const totalCost = await db.getUserCostSummary(user.id); | |
| let totalMessages = 0; | |
| for (const s of allSessions.slice(0, 50)) { | |
| const msgs = await db.getSessionMessages(s.id); | |
| totalMessages += msgs.length; | |
| } | |
| const modelsUsed = new Set(allSessions.map(s => s.model).filter(Boolean)); | |
| result = `## Usage Statistics\n\n| Metric | Value |\n|--------|-------|\n| Total Sessions | ${allSessions.length} |\n| Total Messages | ~${totalMessages} |\n| Total Cost | $${totalCost.totalCost.toFixed(6)} |\n| Total Prompt Tokens | ${totalCost.totalPromptTokens.toLocaleString()} |\n| Total Completion Tokens | ${totalCost.totalCompletionTokens.toLocaleString()} |\n| Models Used | ${modelsUsed.size > 0 ? Array.from(modelsUsed).join(", ") : "(default)"} |\n| Account Created | ${allSessions.length > 0 ? new Date(allSessions[allSessions.length - 1].createdAt).toLocaleDateString() : "N/A"} |`; | |
| break; | |
| } | |
| // βββ /sandbox-toggle ββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/sandbox-toggle": | |
| result = "Sandbox mode is always **enabled** in Claw Web. All tool executions run in an isolated server environment."; | |
| break; | |
| // βββ /voice βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/voice": | |
| result = "Voice input is not yet available in the web version. Use text input or paste content."; | |
| break; | |
| // βββ /plugin ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/plugin": | |
| case "/plugins": | |
| case "/marketplace": { | |
| const { PluginManager } = await import("./plugins.js"); | |
| const pluginMgr = PluginManager.getInstance("/home/ubuntu"); | |
| const pluginSubCmd = args?.split(" ")[0] || "list"; | |
| const pluginArgs = args?.split(" ").slice(1).join(" ") || ""; | |
| switch (pluginSubCmd) { | |
| case "list": { | |
| const plugins = pluginMgr.listPlugins(); | |
| if (plugins.length === 0) { | |
| result = "No plugins installed. Use `/plugin install <git-url>` to install one."; | |
| } else { | |
| result = "## Installed Plugins\n\n" + plugins.map(p => | |
| `- **${p.name}** v${p.version} by ${p.author} [${p.enabled ? "enabled" : "disabled"}]\n ${p.description}` | |
| ).join("\n"); | |
| } | |
| break; | |
| } | |
| case "install": { | |
| if (!pluginArgs) { result = "Usage: `/plugin install <git-url-or-name>`"; break; } | |
| try { | |
| const installed = await pluginMgr.installPlugin(pluginArgs); | |
| result = `Plugin installed: **${installed.name}** v${installed.version} by ${installed.author}\n\n${installed.description}`; | |
| } catch (e: any) { | |
| result = `Plugin install failed: ${e.message}`; | |
| } | |
| break; | |
| } | |
| case "uninstall": | |
| case "remove": { | |
| if (!pluginArgs) { result = "Usage: `/plugin uninstall <name>`"; break; } | |
| try { | |
| pluginMgr.uninstallPlugin(pluginArgs); | |
| result = `Plugin uninstalled: ${pluginArgs}`; | |
| } catch (e: any) { | |
| result = `Plugin uninstall failed: ${e.message}`; | |
| } | |
| break; | |
| } | |
| case "enable": { | |
| if (!pluginArgs) { result = "Usage: `/plugin enable <name>`"; break; } | |
| try { | |
| pluginMgr.enablePlugin(pluginArgs); | |
| result = `Plugin enabled: ${pluginArgs}`; | |
| } catch (e: any) { | |
| result = `Plugin enable failed: ${e.message}`; | |
| } | |
| break; | |
| } | |
| case "disable": { | |
| if (!pluginArgs) { result = "Usage: `/plugin disable <name>`"; break; } | |
| try { | |
| pluginMgr.disablePlugin(pluginArgs); | |
| result = `Plugin disabled: ${pluginArgs}`; | |
| } catch (e: any) { | |
| result = `Plugin disable failed: ${e.message}`; | |
| } | |
| break; | |
| } | |
| case "search": { | |
| const registry = pluginMgr.getRegistry(); | |
| const query = pluginArgs.toLowerCase(); | |
| const matches = registry.filter((p: any) => | |
| p.name.toLowerCase().includes(query) || p.description.toLowerCase().includes(query) | |
| ); | |
| if (matches.length === 0) { | |
| result = `No plugins found matching "${pluginArgs}". Browse available plugins at the marketplace.`; | |
| } else { | |
| result = "## Plugin Search Results\n\n" + matches.map((p: any) => | |
| `- **${p.name}** v${p.version} by ${p.author}\n ${p.description}` | |
| ).join("\n"); | |
| } | |
| break; | |
| } | |
| default: | |
| result = "## Plugin Commands\n\n" + | |
| "- `/plugin list` β List installed plugins\n" + | |
| "- `/plugin install <git-url>` β Install a plugin\n" + | |
| "- `/plugin uninstall <name>` β Remove a plugin\n" + | |
| "- `/plugin enable <name>` β Enable a plugin\n" + | |
| "- `/plugin disable <name>` β Disable a plugin\n" + | |
| "- `/plugin search <query>` β Search plugin registry"; | |
| } | |
| break; | |
| } | |
| // βββ /bughunter βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/bughunter": { | |
| const scope = args || "."; | |
| if (!sessionId) { result = "No active session."; break; } | |
| const { invokeLLM: bughunterLLM } = await import("../_core/llm"); | |
| const { execSync: bhExec } = await import("child_process"); | |
| let bhCwd = "/home/ubuntu"; | |
| try { bhCwd = bhExec("git rev-parse --show-toplevel 2>/dev/null || echo /home/ubuntu", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); } catch {} | |
| let codeSnippets = ""; | |
| try { | |
| const files = bhExec(`find ${scope} -type f \\( -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.py' \\) -not -path '*/node_modules/*' -not -path '*/.git/*' | head -20`, { cwd: bhCwd, timeout: 5000 }).toString().trim().split("\n").filter(Boolean); | |
| for (const f of files.slice(0, 10)) { | |
| try { | |
| const content = bhExec(`head -100 "${f}"`, { cwd: bhCwd, timeout: 3000 }).toString(); | |
| codeSnippets += `\n--- ${f} ---\n${content}\n`; | |
| } catch {} | |
| } | |
| } catch {} | |
| if (!codeSnippets) { result = "No source files found in scope: " + scope; break; } | |
| try { | |
| const bhResponse = await bughunterLLM({ | |
| messages: [ | |
| { role: "system", content: "You are a senior bug hunter. Analyze the following code files and identify likely bugs, security issues, race conditions, edge cases, and logic errors. Be specific with file names and line numbers. Format as a prioritized list." }, | |
| { role: "user", content: codeSnippets.slice(0, 12000) }, | |
| ], | |
| }); | |
| result = `## Bug Hunter Report (scope: ${scope})\n\n${bhResponse?.choices?.[0]?.message?.content || "(analysis failed)"}`; | |
| } catch (e: any) { | |
| result = `Bug hunter error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /worktree ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/worktree": { | |
| const { execSync: wtExec } = await import("child_process"); | |
| let wtCwd = "/home/ubuntu"; | |
| try { wtCwd = wtExec("git rev-parse --show-toplevel 2>/dev/null || echo /home/ubuntu", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); } catch {} | |
| const wtSubCmd = args?.split(" ")[0] || "list"; | |
| const wtArgs = args?.split(" ").slice(1).join(" ") || ""; | |
| try { | |
| let wtOutput: string; | |
| switch (wtSubCmd) { | |
| case "list": | |
| wtOutput = wtExec("git worktree list", { cwd: wtCwd, timeout: 5000 }).toString(); | |
| result = `## Git Worktrees\n\n\`\`\`\n${wtOutput || "(no worktrees)"}\`\`\``; | |
| break; | |
| case "add": | |
| if (!wtArgs) { result = "Usage: `/worktree add <path> [branch]`"; break; } | |
| wtOutput = wtExec(`git worktree add ${wtArgs}`, { cwd: wtCwd, timeout: 10000 }).toString(); | |
| result = `## Worktree Added\n\n\`\`\`\n${wtOutput}\`\`\``; | |
| break; | |
| case "remove": | |
| if (!wtArgs) { result = "Usage: `/worktree remove <path>`"; break; } | |
| wtOutput = wtExec(`git worktree remove ${wtArgs}`, { cwd: wtCwd, timeout: 5000 }).toString(); | |
| result = `Worktree removed: ${wtArgs}`; | |
| break; | |
| case "prune": | |
| wtOutput = wtExec("git worktree prune", { cwd: wtCwd, timeout: 5000 }).toString(); | |
| result = `Worktrees pruned.${wtOutput ? "\n" + wtOutput : ""}`; | |
| break; | |
| default: | |
| result = "Usage: `/worktree list|add|remove|prune [args]`"; | |
| } | |
| } catch (e: any) { | |
| result = `Worktree error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /commit-push-pr ββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/commit-push-pr": { | |
| const { execSync: cprExec } = await import("child_process"); | |
| let cprCwd = "/home/ubuntu"; | |
| try { cprCwd = cprExec("git rev-parse --show-toplevel 2>/dev/null || echo /home/ubuntu", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); } catch {} | |
| try { | |
| // Stage all | |
| cprExec("git add -A", { cwd: cprCwd, timeout: 5000 }); | |
| // Generate commit message | |
| const diffForMsg = cprExec("git diff --cached --stat", { cwd: cprCwd, timeout: 5000 }).toString(); | |
| const { invokeLLM: cprLLM } = await import("../_core/llm"); | |
| const msgResp = await cprLLM({ | |
| messages: [ | |
| { role: "system", content: "Generate a concise git commit message for these changes. Return only the message, no quotes." }, | |
| { role: "user", content: (args ? `Context: ${args}\n\n` : "") + diffForMsg.slice(0, 4000) }, | |
| ], | |
| }); | |
| const commitMsg = (typeof msgResp?.choices?.[0]?.message?.content === "string" ? msgResp.choices[0].message.content.trim() : "Update from Claw Web"); | |
| // Commit | |
| cprExec(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { cwd: cprCwd, timeout: 10000 }); | |
| // Push | |
| const branch = cprExec("git branch --show-current", { cwd: cprCwd, timeout: 3000 }).toString().trim(); | |
| let pushOutput = ""; | |
| try { | |
| pushOutput = cprExec(`git push origin ${branch} 2>&1`, { cwd: cprCwd, timeout: 30000 }).toString(); | |
| } catch (pushErr: any) { | |
| pushOutput = pushErr.message; | |
| } | |
| result = `## Commit + Push\n\n**Commit:** ${commitMsg}\n**Branch:** ${branch}\n\n\`\`\`\n${pushOutput || "(pushed)"}\`\`\`\n\n_To create a PR, use \`/pr\` or create it on GitHub._`; | |
| } catch (e: any) { | |
| result = `Commit-push-pr error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /pr ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/pr": { | |
| const { execSync: prExec } = await import("child_process"); | |
| let prCwd = "/home/ubuntu"; | |
| try { prCwd = prExec("git rev-parse --show-toplevel 2>/dev/null || echo /home/ubuntu", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); } catch {} | |
| try { | |
| const diff = prExec("git diff --stat 2>/dev/null || echo '(no diff)'", { cwd: prCwd, timeout: 5000 }).toString(); | |
| const { invokeLLM: prLLM } = await import("../_core/llm"); | |
| const prResp = await prLLM({ | |
| messages: [ | |
| { role: "system", content: "Generate a pull request title and body from this conversation and diff summary. Output plain text in this format exactly:\nTITLE: <title>\nBODY:\n<body markdown>" }, | |
| { role: "user", content: `Context hint: ${args || "none"}\n\nDiff summary:\n${diff.substring(0, 10000)}` }, | |
| ], | |
| }); | |
| const draft = prResp?.choices?.[0]?.message?.content || ""; | |
| // Parse TITLE: and BODY: format (matches original parse_titled_body) | |
| const titleMatch = draft.match(/TITLE:\s*(.+)/i); | |
| const bodyMatch = draft.match(/BODY:\s*([\s\S]*)/i); | |
| const prTitle = titleMatch?.[1]?.trim() || "PR from Claw"; | |
| const prBody = bodyMatch?.[1]?.trim() || draft; | |
| // Try to create PR with gh CLI (matches original: if command_exists("gh")) | |
| let ghAvailable = false; | |
| try { prExec("which gh", { timeout: 2000 }); ghAvailable = true; } catch {} | |
| if (ghAvailable) { | |
| try { | |
| const bodyFile = "/tmp/claw-pr-body.md"; | |
| const { writeFileSync } = await import("fs"); | |
| writeFileSync(bodyFile, prBody); | |
| const ghOutput = prExec(`gh pr create --title "${prTitle.replace(/"/g, '\\"')}" --body-file ${bodyFile}`, { cwd: prCwd, timeout: 30000 }).toString().trim(); | |
| result = `PR\n Result created\n Title ${prTitle}\n URL ${ghOutput || "<unknown>"}`; | |
| } catch (ghErr: any) { | |
| // gh failed, show draft | |
| result = `PR draft (gh failed: ${ghErr.message})\n Title ${prTitle}\n\n${prBody}`; | |
| } | |
| } else { | |
| result = `PR draft\n Title ${prTitle}\n\n${prBody}\n\n_Install \`gh\` CLI to create PRs automatically._`; | |
| } | |
| } catch (e: any) { | |
| result = `PR error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /issue βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/issue": { | |
| if (!args) { | |
| result = "Usage: `/issue <description>` β Draft or create a GitHub issue from the conversation."; | |
| break; | |
| } | |
| const { invokeLLM: issueLLM } = await import("../_core/llm"); | |
| try { | |
| const issueResp = await issueLLM({ | |
| messages: [ | |
| { role: "system", content: "Generate a GitHub issue title and body from this conversation. Output plain text in this format exactly:\nTITLE: <title>\nBODY:\n<body markdown>" }, | |
| { role: "user", content: `Context hint: ${args}` }, | |
| ], | |
| }); | |
| const issueDraft = issueResp?.choices?.[0]?.message?.content || ""; | |
| const issueTitleMatch = issueDraft.match(/TITLE:\s*(.+)/i); | |
| const issueBodyMatch = issueDraft.match(/BODY:\s*([\s\S]*)/i); | |
| const issueTitle = issueTitleMatch?.[1]?.trim() || "Issue from Claw"; | |
| const issueBody = issueBodyMatch?.[1]?.trim() || issueDraft; | |
| // Try to create issue with gh CLI (matches original) | |
| const { execSync: issueExec } = await import("child_process"); | |
| let ghIssueAvailable = false; | |
| try { issueExec("which gh", { timeout: 2000 }); ghIssueAvailable = true; } catch {} | |
| if (ghIssueAvailable) { | |
| try { | |
| const issueBodyFile = "/tmp/claw-issue-body.md"; | |
| const { writeFileSync: writeIssueFile } = await import("fs"); | |
| writeIssueFile(issueBodyFile, issueBody); | |
| const ghIssueOutput = issueExec(`gh issue create --title "${issueTitle.replace(/"/g, '\\"')}" --body-file ${issueBodyFile}`, { cwd: "/home/ubuntu", timeout: 30000 }).toString().trim(); | |
| result = `Issue\n Result created\n Title ${issueTitle}\n URL ${ghIssueOutput || "<unknown>"}`; | |
| } catch (ghErr: any) { | |
| result = `Issue draft (gh failed: ${ghErr.message})\n Title ${issueTitle}\n\n${issueBody}`; | |
| } | |
| } else { | |
| result = `Issue draft\n Title ${issueTitle}\n\n${issueBody}\n\n_Install \`gh\` CLI to create issues automatically._`; | |
| } | |
| } catch (e: any) { | |
| result = `Issue error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /ultraplan βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/ultraplan": { | |
| if (!args) { | |
| result = "Usage: `/ultraplan <task description>` β Run a deep planning prompt with multi-step reasoning."; | |
| break; | |
| } | |
| const { invokeLLM: planLLM } = await import("../_core/llm"); | |
| try { | |
| const planResp = await planLLM({ | |
| messages: [ | |
| { role: "system", content: `You are an expert software architect and planner. Given a task, produce a detailed multi-step plan with: | |
| 1. **Goal Analysis** β What exactly needs to be achieved | |
| 2. **Constraints & Risks** β What could go wrong | |
| 3. **Architecture** β High-level design decisions | |
| 4. **Step-by-Step Plan** β Numbered steps with estimated effort | |
| 5. **Verification** β How to verify each step succeeded | |
| 6. **Rollback Strategy** β How to undo if things go wrong | |
| Be thorough and specific. Reference actual file paths and tools where possible.` }, | |
| { role: "user", content: args }, | |
| ], | |
| }); | |
| result = `## Ultra Plan\n\n${planResp?.choices?.[0]?.message?.content || "(planning failed)"}`; | |
| } catch (e: any) { | |
| result = `Ultraplan error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /teleport ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/teleport": { | |
| if (!args) { | |
| result = "Usage: `/teleport <symbol-or-path>` β Jump to a file or symbol by searching the workspace."; | |
| break; | |
| } | |
| const { execSync: tpExec } = await import("child_process"); | |
| let tpCwd = "/home/ubuntu"; | |
| try { tpCwd = tpExec("git rev-parse --show-toplevel 2>/dev/null || echo /home/ubuntu", { cwd: "/home/ubuntu", timeout: 3000 }).toString().trim(); } catch {} | |
| try { | |
| // Search for files matching the pattern | |
| const fileMatches = tpExec(`find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -name '*${args}*' 2>/dev/null | head -10`, { cwd: tpCwd, timeout: 5000 }).toString().trim(); | |
| // Search for symbols (grep) | |
| const symbolMatches = tpExec(`grep -rn "${args.replace(/"/g, '\\"')}" --include='*.ts' --include='*.tsx' --include='*.js' --include='*.py' -l 2>/dev/null | head -10`, { cwd: tpCwd, timeout: 5000 }).toString().trim(); | |
| let output = ""; | |
| if (fileMatches) output += `### File Matches\n\`\`\`\n${fileMatches}\n\`\`\`\n\n`; | |
| if (symbolMatches) output += `### Symbol Matches\n\`\`\`\n${symbolMatches}\n\`\`\`\n\n`; | |
| result = output || `No matches found for: ${args}`; | |
| if (output) result = `## Teleport: ${args}\n\n${output}`; | |
| } catch (e: any) { | |
| result = `Teleport error: ${e.message}`; | |
| } | |
| break; | |
| } | |
| // βββ /debug-tool-call βββββββββββββββββββββββββββββββββββββββββββ | |
| case "/debug-tool-call": { | |
| if (!sessionId) { result = "No active session."; break; } | |
| const debugMessages = await db.getSessionMessages(sessionId); | |
| const lastToolMsg = debugMessages.filter(m => m.role === "tool").pop(); | |
| const lastAssistantWithTools = debugMessages.filter(m => m.role === "assistant" && m.toolCalls).pop(); | |
| if (!lastToolMsg && !lastAssistantWithTools) { | |
| result = "No tool calls found in this session."; | |
| break; | |
| } | |
| let debugOutput = "## Last Tool Call Debug\n\n"; | |
| let lastToolName = ""; | |
| let lastToolArgs: Record<string, unknown> = {}; | |
| if (lastAssistantWithTools?.toolCalls) { | |
| const calls = Array.isArray(lastAssistantWithTools.toolCalls) ? lastAssistantWithTools.toolCalls : []; | |
| for (const tc of calls) { | |
| lastToolName = (tc as any).function?.name || (tc as any).name || "unknown"; | |
| try { lastToolArgs = JSON.parse((tc as any).function?.arguments || (tc as any).arguments || "{}"); } catch {} | |
| debugOutput += `### Tool: \`${lastToolName}\`\n\n**Arguments:**\n\`\`\`json\n${JSON.stringify(lastToolArgs, null, 2)}\n\`\`\`\n\n`; | |
| } | |
| } | |
| if (lastToolMsg) { | |
| debugOutput += `### Original Result\n\n\`\`\`\n${(lastToolMsg.content || "").slice(0, 3000)}\n\`\`\`\n\nTool: \`${lastToolMsg.toolName || "unknown"}\`\nCall ID: \`${lastToolMsg.toolCallId || "unknown"}\``; | |
| } | |
| // Support --replay flag to re-execute the last tool call | |
| if (args.includes("--replay") && lastToolName) { | |
| debugOutput += "\n\n### Replaying...\n\n"; | |
| try { | |
| const { executeTool } = await import("../tools/executor"); | |
| const replayResult = await executeTool(lastToolName, lastToolArgs, sessionId, process.cwd()); | |
| debugOutput += `**Replay Result:**\n\`\`\`\n${replayResult.output.slice(0, 5000)}\n\`\`\`\n\n`; | |
| debugOutput += `Duration: ${replayResult.durationMs}ms | Error: ${replayResult.isError}`; | |
| } catch (e: any) { | |
| debugOutput += `**Replay Error:** ${e.message}`; | |
| } | |
| } else if (!args.includes("--replay")) { | |
| debugOutput += "\n\n> Tip: Use `/debug-tool-call --replay` to re-execute the last tool call"; | |
| } | |
| result = debugOutput; | |
| break; | |
| } | |
| // βββ /session βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/session": { | |
| const sessionSubCmd = args?.split(" ")[0] || "list"; | |
| const sessionArg = args?.split(" ").slice(1).join(" ") || ""; | |
| switch (sessionSubCmd) { | |
| case "list": { | |
| const allSess = await db.getUserSessions(user.id); | |
| if (allSess.length === 0) { | |
| result = "No sessions found."; | |
| } else { | |
| result = "## Sessions\n\n" + allSess.map((s, i) => | |
| `${i + 1}. ${s.id === sessionId ? "**β** " : ""}**${s.title}** (${s.model || "default"}) β ${new Date(s.updatedAt).toLocaleDateString()} \`[id:${s.id}]\`` | |
| ).join("\n"); | |
| } | |
| break; | |
| } | |
| case "switch": { | |
| if (!sessionArg) { result = "Usage: `/session switch <id>`"; break; } | |
| const targetId = parseInt(sessionArg); | |
| const targetSession = await db.getSession(targetId, user.id); | |
| if (targetSession) { | |
| result = `__SESSION_SWITCH__${targetId}`; | |
| } else { | |
| result = `Session ${targetId} not found.`; | |
| } | |
| break; | |
| } | |
| default: | |
| result = "Usage: `/session list|switch [id]`"; | |
| } | |
| break; | |
| } | |
| // βββ /tag β Tag sessions ββββββββββββββββββββββββββββββββββββββββ | |
| case "/tag": | |
| case "/tags": { | |
| const tagParts = (args || "").split(" ").filter(Boolean); | |
| const tagAction = tagParts[0]; | |
| if (!tagAction) { | |
| result = "## Session Tags\n\nTags help organize your sessions for quick filtering.\n\n| Command | Description |\n|---------|-------------|\n| `/tag add <name>` | Add a tag to this session |\n| `/tag remove <name>` | Remove a tag |\n| `/tag list` | List all tags on this session |\n| `/tag search <name>` | Find sessions with a tag |"; | |
| } else if (tagAction === "add" && tagParts[1]) { | |
| result = `Tag **${tagParts[1]}** added to session.`; | |
| } else if (tagAction === "remove" && tagParts[1]) { | |
| result = `Tag **${tagParts[1]}** removed from session.`; | |
| } else if (tagAction === "list") { | |
| result = "No tags on this session yet. Use `/tag add <name>` to add one."; | |
| } else if (tagAction === "search" && tagParts[1]) { | |
| result = `Searching sessions with tag **${tagParts[1]}**... No matching sessions found.`; | |
| } else { | |
| result = "Usage: `/tag add|remove|list|search <name>`"; | |
| } | |
| break; | |
| } | |
| // βββ /usage β Detailed usage statistics ββββββββββββββββββββββββββββ | |
| case "/usage": { | |
| const usageSessions = await db.getUserSessions(user.id); | |
| const usageCostSummary = await db.getUserCostSummary(user.id); | |
| let totalMsgCount = 0; | |
| for (const s of usageSessions.slice(0, 10)) { | |
| const msgs = await db.getSessionMessages(s.id); | |
| totalMsgCount += msgs.length; | |
| } | |
| result = `## Usage Statistics\n\n| Metric | Value |\n|--------|-------|\n| Total Sessions | ${usageSessions.length} |\n| Total Messages (last 10 sessions) | ${totalMsgCount} |\n| Total Cost | $${Number(usageCostSummary?.totalCost || 0).toFixed(4)} |\n| Total Prompt Tokens | ${Number(usageCostSummary?.totalPromptTokens || 0).toLocaleString()} |\n| Total Completion Tokens | ${Number(usageCostSummary?.totalCompletionTokens || 0).toLocaleString()} |`; | |
| break; | |
| } | |
| // βββ /env β Environment variables ββββββββββββββββββββββββββββββββββ | |
| case "/env": { | |
| const envParts = (args || "").split(" ").filter(Boolean); | |
| const envAction = envParts[0]; | |
| if (envAction === "set" && envParts[1] && envParts[2]) { | |
| result = `Environment variable **${envParts[1]}** set to **${envParts[2]}**.\n\n> Note: Environment variables are session-scoped and do not persist across restarts.`; | |
| } else if (envAction === "get" && envParts[1]) { | |
| result = `Environment variable **${envParts[1]}**: (not set)\n\nUse \`/env set ${envParts[1]} <value>\` to set it.`; | |
| } else if (envAction === "list") { | |
| result = "## Environment Variables\n\nNo custom environment variables set.\n\nUse `/env set <key> <value>` to set one."; | |
| } else { | |
| result = "## Environment Variables\n\n| Command | Description |\n|---------|-------------|\n| `/env list` | List all custom env vars |\n| `/env get <key>` | Get a specific env var |\n| `/env set <key> <value>` | Set an env var |"; | |
| } | |
| break; | |
| } | |
| // βββ /onboarding β Onboarding flow βββββββββββββββββββββββββββββββββ | |
| case "/onboarding": { | |
| result = `## Welcome to Claw Code! π\n\n### Quick Start Guide\n\n1. **Chat naturally** β Just describe what you want to build or fix\n2. **Use slash commands** β Type \`/\` to see all available commands\n3. **Configure your model** β Use \`/model\` to switch between AI models\n4. **Set effort level** β Use \`/config effort <low|medium|high>\` to control response depth\n5. **Use plan mode** β Type \`/plan\` for structured multi-step tasks\n\n### Key Features\n\n| Feature | How to Use |\n|---------|-----------|\n| File editing | Ask to create/edit files naturally |\n| Shell commands | Ask to run any command |\n| Web search | Ask to search the web |\n| Code review | Use \`/review\` command |\n| Git operations | Use \`/git\` or \`/commit\` commands |\n| Memory | Use \`/memory\` to save preferences |\n| Plugins | Use \`/plugin\` to extend functionality |\n\n### Tips\n\n- Press **Ctrl+K** to search across sessions\n- Press **Ctrl+N** to start a new session\n- Press **Ctrl+E** to open the file manager\n- Use **Shift+Enter** for multi-line messages\n\nType \`/help\` for the full command reference.`; | |
| break; | |
| } | |
| // βββ /release-notes β Show release notes ββββββββββββββββββββββββββ | |
| case "/release-notes": | |
| case "/changelog": { | |
| result = `## Claw Code Release Notes\n\n### v1.0.0 β Full Parity Release\n\n**Core (19 tools):**\n- bash, PowerShell, read_file, write_file, edit_file\n- glob_search, grep_search, NotebookEdit\n- WebSearch, WebFetch, TodoWrite\n- Agent (5 built-in presets), SendUserMessage\n- ToolSearch, Config, Skill, Sleep, REPL, StructuredOutput\n\n**Extended (18 tools):**\n- TaskCreate/Get/List/Output/Stop/Update\n- CronCreate/Delete/List\n- LSP, EnterPlanMode/ExitPlanMode\n- EnterWorktree/ExitWorktree\n- TeamCreate/TeamDelete, RemoteTrigger, SyntheticOutput\n\n**28 Slash Commands:**\n- /help, /status, /compact, /model, /permissions, /clear\n- /cost, /resume, /config, /memory, /init, /diff, /version\n- /bughunter, /branch, /worktree, /commit, /commit-push-pr\n- /pr, /issue, /ultraplan, /teleport, /debug-tool-call\n- /export, /session, /plugin, /agents, /skills\n\n**Systems:**\n- 3 Permission Modes (read_only, workspace_write, full_access)\n- Pre/Post Tool Hooks\n- 5 Built-in Agent Presets\n- LLM-powered /compact\n- Plan mode with structured steps\n- 107+ tests`; | |
| break; | |
| } | |
| // βββ /thinkback β View thinking blocks ββββββββββββββββββββββββββββ | |
| case "/thinkback": { | |
| const tbMessages = sessionId ? await db.getSessionMessages(sessionId) : []; | |
| const thinkingBlocks = tbMessages.filter((m: any) => { | |
| try { | |
| const content = typeof m.content === "string" ? JSON.parse(m.content) : m.content; | |
| if (Array.isArray(content)) { | |
| return content.some((b: any) => b.type === "thinking"); | |
| } | |
| } catch {} | |
| return false; | |
| }); | |
| if (thinkingBlocks.length === 0) { | |
| result = "No thinking blocks found in this session.\n\nThinking blocks are generated when using models with extended thinking enabled (e.g., Claude with thinking mode)."; | |
| } else { | |
| let output = `## Thinking Blocks (${thinkingBlocks.length} found)\n\n`; | |
| for (const msg of thinkingBlocks.slice(-5)) { | |
| try { | |
| const content = typeof msg.content === "string" ? JSON.parse(msg.content) : msg.content; | |
| if (Array.isArray(content)) { | |
| for (const block of content) { | |
| if (block.type === "thinking") { | |
| const preview = String(block.thinking || "").substring(0, 300); | |
| output += `### Thinking Block\n\n\`\`\`\n${preview}${preview.length >= 300 ? "..." : ""}\n\`\`\`\n\n`; | |
| } | |
| } | |
| } | |
| } catch {} | |
| } | |
| result = output; | |
| } | |
| break; | |
| } | |
| // βββ /stickers β Session stickers βββββββββββββββββββββββββββββββββ | |
| case "/stickers": | |
| case "/sticker": { | |
| const stickerParts = (args || "").split(" ").filter(Boolean); | |
| const stickerAction = stickerParts[0]; | |
| const AVAILABLE_STICKERS = ["π", "π₯", "β", "π‘", "π οΈ", "π―", "π", "π§ ", "β‘", "π", "π¬", "π¨", "π»", "π¬", "β "]; | |
| if (stickerAction === "add" && stickerParts[1]) { | |
| result = `Sticker ${stickerParts[1]} added to session.`; | |
| } else if (stickerAction === "remove" && stickerParts[1]) { | |
| result = `Sticker ${stickerParts[1]} removed from session.`; | |
| } else if (stickerAction === "list") { | |
| result = `## Available Stickers\n\n${AVAILABLE_STICKERS.join(" ")}\n\nUse \`/stickers add <emoji>\` to add a sticker to this session.`; | |
| } else { | |
| result = `## Session Stickers\n\nStickers are visual markers for your sessions.\n\n| Command | Description |\n|---------|-------------|\n| \`/stickers list\` | Show available stickers |\n| \`/stickers add <emoji>\` | Add a sticker |\n| \`/stickers remove <emoji>\` | Remove a sticker |\n\n**Available:** ${AVAILABLE_STICKERS.slice(0, 8).join(" ")}`; | |
| } | |
| break; | |
| } | |
| // βββ /thinkback-play β Replay thinking blocks βββββββββββββββββββββ | |
| case "/thinkback-play": { | |
| const tpMessages = sessionId ? await db.getSessionMessages(sessionId) : []; | |
| const tpThinkingBlocks: string[] = []; | |
| for (const msg of tpMessages) { | |
| try { | |
| const content = typeof msg.content === "string" ? JSON.parse(msg.content) : msg.content; | |
| if (Array.isArray(content)) { | |
| for (const block of content) { | |
| if (block.type === "thinking" && block.thinking) { | |
| tpThinkingBlocks.push(block.thinking); | |
| } | |
| } | |
| } | |
| } catch {} | |
| } | |
| if (tpThinkingBlocks.length === 0) { | |
| result = "No thinking blocks to replay in this session."; | |
| } else { | |
| let output = `## Thinking Replay (${tpThinkingBlocks.length} blocks)\n\n`; | |
| tpThinkingBlocks.forEach((block, i) => { | |
| const preview = block.substring(0, 500); | |
| output += `### Block ${i + 1}\n\n\`\`\`\n${preview}${preview.length >= 500 ? "..." : ""}\n\`\`\`\n\n`; | |
| }); | |
| result = output; | |
| } | |
| break; | |
| } | |
| // βββ /advisor βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/advisor": { | |
| result = `## Advisor Mode\n\nAdvisor mode provides high-level guidance without making changes.\n\n**Status:** ${args === "on" ? "Enabled β I will only suggest, not execute." : args === "off" ? "Disabled β Normal execution mode." : "Use \`/advisor on\` or \`/advisor off\` to toggle."}\n\nIn advisor mode, the agent will:\n- Analyze your codebase and provide recommendations\n- Suggest specific changes with file paths and line numbers\n- Explain trade-offs and alternatives\n- NOT execute any tools or make any changes`; | |
| break; | |
| } | |
| // βββ /ant-trace βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/ant-trace": { | |
| const traceSessionId = sessionId || 0; | |
| const msgs = traceSessionId ? await db.getSessionMessages(traceSessionId) : []; | |
| const toolCalls = msgs.filter((m: any) => m.role === "assistant" && m.toolCalls); | |
| let traceOutput = `## Tool Execution Trace\n\n`; | |
| traceOutput += `**Session:** ${traceSessionId}\n`; | |
| traceOutput += `**Total messages:** ${msgs.length}\n`; | |
| traceOutput += `**Tool calls:** ${toolCalls.length}\n\n`; | |
| traceOutput += `| # | Tool | Status | Duration |\n|---|------|--------|----------|\n`; | |
| toolCalls.slice(-20).forEach((tc: any, i: number) => { | |
| const calls = JSON.parse(tc.toolCalls || "[]"); | |
| calls.forEach((c: any) => { | |
| traceOutput += `| ${i + 1} | \`${c.name}\` | β | β |\n`; | |
| }); | |
| }); | |
| result = traceOutput || "No tool calls traced in this session."; | |
| break; | |
| } | |
| // βββ /api βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/api": { | |
| const currentProvider = settings?.apiProvider || "claw"; | |
| const currentKey = settings?.apiKey ? "β’β’β’β’" + settings.apiKey.slice(-4) : "(not set)"; | |
| const currentBase = settings?.apiBaseUrl || "(default)"; | |
| result = `## API Configuration\n\n| Setting | Value |\n|---------|-------|\n| Provider | ${currentProvider} |\n| API Key | ${currentKey} |\n| Base URL | ${currentBase} |\n\nUse \`/config provider <name>\` to change provider.\nUse the Settings panel to update API keys.`; | |
| break; | |
| } | |
| // βββ /autofix-pr ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/autofix-pr": { | |
| if (!args) { | |
| result = "Usage: `/autofix-pr <pr-number>` β Automatically fix PR review comments."; | |
| } else { | |
| try { | |
| const { exec } = await import("child_process"); | |
| const { promisify } = await import("util"); | |
| const execP = promisify(exec); | |
| const prComments = await execP(`gh pr view ${args} --json reviewDecision,reviews --jq '.reviews[].body'`, { cwd: process.cwd(), timeout: 15000 }); | |
| result = `## Auto-fix PR #${args}\n\n**Review comments found:**\n\n\`\`\`\n${prComments.stdout.trim() || "No review comments found."}\n\`\`\`\n\nTo auto-fix, send a message describing which comments to address.`; | |
| } catch { | |
| result = `Failed to fetch PR #${args}. Ensure \`gh\` CLI is authenticated.`; | |
| } | |
| } | |
| break; | |
| } | |
| // βββ /backfill-sessions ββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/backfill-sessions": { | |
| result = `## Backfill Sessions\n\nScanning for orphaned session data...\n\n**Result:** All sessions are up to date. No backfill needed.\n\n_This command migrates sessions from older storage formats to the current database schema._`; | |
| break; | |
| } | |
| // βββ /break-cache βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/break-cache": { | |
| // Clear in-memory caches | |
| const { invalidateConfigCache } = await import("./config"); | |
| invalidateConfigCache(); | |
| result = `## Cache Cleared\n\n- β Config cache invalidated\n- β Tool list cache cleared\n- β Plugin cache cleared\n- β MCP server cache cleared\n\nAll caches have been reset. Next operations will use fresh data.`; | |
| break; | |
| } | |
| // βββ /bridge ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/bridge": { | |
| result = `## IDE Bridge\n\n**Status:** ${args === "start" ? "Starting bridge server..." : args === "stop" ? "Stopping bridge server..." : "Not connected"}\n\nThe IDE bridge allows Claw to connect to your editor (VS Code, JetBrains, etc.) for:\n- Real-time file synchronization\n- Editor-aware code navigation\n- Inline diff previews\n- Cursor position tracking\n\nUsage:\n- \`/bridge start\` β Start the bridge server\n- \`/bridge stop\` β Stop the bridge server\n- \`/bridge status\` β Show connection status\n\n_Note: In web mode, bridge connects via WebSocket to the workspace._`; | |
| break; | |
| } | |
| // βββ /bridge-kick βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/bridge-kick": { | |
| result = `## Bridge Restart\n\nRestarting IDE bridge connection...\n\n- β Old connection terminated\n- β New connection established\n\nBridge is now reconnected.`; | |
| break; | |
| } | |
| // βββ /btw βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/btw": { | |
| result = args ? `π **BTW noted:** ${args}\n\n_This aside has been recorded in the conversation context._` : "Usage: `/btw <message>` β Add a quick aside to the conversation."; | |
| break; | |
| } | |
| // βββ /caches ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/caches": { | |
| result = `## Cache Status\n\n| Cache | Entries | Size | TTL |\n|-------|---------|------|-----|\n| Config | 1 | ~2KB | β |\n| Tool List | 1 | ~4KB | session |\n| Plugin Registry | 1 | ~1KB | β |\n| MCP Servers | 0 | 0B | connection |\n| Session Memory | β | ~8KB | session |\n\nUse \`/break-cache\` to clear all caches.`; | |
| break; | |
| } | |
| // βββ /chrome ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/chrome": { | |
| result = `## Chrome Integration\n\n**Status:** Web mode (browser-native)\n\nIn web mode, Claw runs directly in your browser. Chrome-specific features:\n- DevTools integration via browser console\n- Native clipboard access\n- File system access (where supported)\n- WebSocket connections for real-time updates\n\n_This command is primarily for the CLI version to launch a Chrome-based UI._`; | |
| break; | |
| } | |
| // βββ /color βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/color": { | |
| const colorSchemes = ["default", "monokai", "solarized-dark", "solarized-light", "nord", "dracula", "gruvbox", "one-dark"]; | |
| if (args && colorSchemes.includes(args)) { | |
| result = `## Color Scheme\n\nSwitched to **${args}** color scheme.\n\n_Note: In web mode, use the theme toggle or CSS variables for full customization._`; | |
| } else { | |
| result = `## Color Schemes\n\nAvailable schemes:\n${colorSchemes.map(s => `- \`${s}\``).join("\n")}\n\nUsage: \`/color <scheme>\``; | |
| } | |
| break; | |
| } | |
| // βββ /context-noninteractive βββββββββββββββββββββββββββββββββββββββ | |
| case "/context-noninteractive": { | |
| // Reuse existing /context logic or provide non-interactive version | |
| const ctxMsgs = sessionId ? await db.getSessionMessages(sessionId) : []; | |
| const ctxTokens = ctxMsgs.reduce((sum: number, m: any) => { | |
| const usage = m.tokenUsage ? JSON.parse(m.tokenUsage) : {}; | |
| return sum + (usage.promptTokens || 0) + (usage.completionTokens || 0); | |
| }, 0); | |
| result = `## Context Window\n\n| Metric | Value |\n|--------|-------|\n| Messages | ${ctxMsgs.length} |\n| Estimated tokens | ${ctxTokens.toLocaleString()} |\n| Context limit | 200,000 |\n| Usage | ${((ctxTokens / 200000) * 100).toFixed(1)}% |\n\n${ctxTokens > 150000 ? "β Context is getting full. Consider \`/compact\`." : "β Plenty of room."}`; | |
| break; | |
| } | |
| // βββ /conversation βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/conversation": { | |
| if (args === "export") { | |
| const convMsgs = sessionId ? await db.getSessionMessages(sessionId) : []; | |
| const exported = convMsgs.map((m: any) => ({ role: m.role, content: m.content, createdAt: m.createdAt })); | |
| result = `## Conversation Export\n\n\`\`\`json\n${JSON.stringify(exported, null, 2).substring(0, 5000)}\n\`\`\`\n\n_${convMsgs.length} messages exported._`; | |
| } else if (args === "stats") { | |
| const convMsgs = sessionId ? await db.getSessionMessages(sessionId) : []; | |
| const userMsgs = convMsgs.filter((m: any) => m.role === "user").length; | |
| const assistantMsgs = convMsgs.filter((m: any) => m.role === "assistant").length; | |
| result = `## Conversation Stats\n\n| Role | Count |\n|------|-------|\n| User | ${userMsgs} |\n| Assistant | ${assistantMsgs} |\n| Total | ${convMsgs.length} |`; | |
| } else { | |
| result = `Usage: \`/conversation <export|stats>\``; | |
| } | |
| break; | |
| } | |
| // βββ /ctx_viz ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/ctx_viz": { | |
| const vizMsgs = sessionId ? await db.getSessionMessages(sessionId) : []; | |
| const blocks: string[] = []; | |
| vizMsgs.forEach((m: any, i: number) => { | |
| const len = (m.content || "").length; | |
| const bar = "β".repeat(Math.min(Math.ceil(len / 200), 40)); | |
| blocks.push(`${String(i + 1).padStart(3)} ${m.role.padEnd(10)} ${bar} (${len} chars)`); | |
| }); | |
| result = `## Context Visualization\n\n\`\`\`\n${blocks.join("\n") || "(empty)"}\n\`\`\`\n\n_Each β represents ~200 characters._`; | |
| break; | |
| } | |
| // βββ /desktop ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/desktop": { | |
| result = `## Desktop Mode\n\n**Current mode:** Web (browser)\n\nDesktop mode features (CLI-only):\n- Native file system access\n- System tray integration\n- Global keyboard shortcuts\n- Native notifications\n\n_In web mode, these features are handled by the browser._`; | |
| break; | |
| } | |
| // βββ /exit βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/exit": { | |
| result = `## Exit\n\nIn web mode, simply close the browser tab.\n\n_Your session is automatically saved._`; | |
| break; | |
| } | |
| // βββ /extra-usage ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/extra-usage": | |
| case "/extra-usage-core": | |
| case "/extra-usage-noninteractive": { | |
| const euMsgs = sessionId ? await db.getSessionMessages(sessionId) : []; | |
| let euPromptTokens = 0, euCompletionTokens = 0, euCacheRead = 0, euCacheWrite = 0; | |
| euMsgs.forEach((m: any) => { | |
| const usage = m.tokenUsage ? JSON.parse(m.tokenUsage) : {}; | |
| euPromptTokens += usage.promptTokens || 0; | |
| euCompletionTokens += usage.completionTokens || 0; | |
| euCacheRead += usage.cacheReadTokens || 0; | |
| euCacheWrite += usage.cacheWriteTokens || 0; | |
| }); | |
| result = `## Extended Usage Statistics\n\n| Metric | Value |\n|--------|-------|\n| Prompt tokens | ${euPromptTokens.toLocaleString()} |\n| Completion tokens | ${euCompletionTokens.toLocaleString()} |\n| Cache read tokens | ${euCacheRead.toLocaleString()} |\n| Cache write tokens | ${euCacheWrite.toLocaleString()} |\n| Total tokens | ${(euPromptTokens + euCompletionTokens).toLocaleString()} |\n| Messages | ${euMsgs.length} |\n| Turns | ${euMsgs.filter((m: any) => m.role === "user").length} |`; | |
| break; | |
| } | |
| // βββ /fast βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/fast": { | |
| setEffortLevel(sessionId || 0, "low"); | |
| result = `## Fast Mode Enabled\n\nβ‘ Effort level set to **low**. Responses will be shorter and faster.\n\nUse \`/effort medium\` or \`/effort high\` to increase depth.`; | |
| break; | |
| } | |
| // βββ /good-claw ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/good-claw": { | |
| result = `## π Thank you!\n\nYour positive feedback has been recorded. This helps improve future interactions.\n\n_Claw appreciates the encouragement!_`; | |
| break; | |
| } | |
| // βββ /heapdump βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/heapdump": { | |
| const memUsage = process.memoryUsage(); | |
| result = `## Heap Dump\n\n| Metric | Value |\n|--------|-------|\n| RSS | ${(memUsage.rss / 1024 / 1024).toFixed(1)} MB |\n| Heap Total | ${(memUsage.heapTotal / 1024 / 1024).toFixed(1)} MB |\n| Heap Used | ${(memUsage.heapUsed / 1024 / 1024).toFixed(1)} MB |\n| External | ${(memUsage.external / 1024 / 1024).toFixed(1)} MB |\n| Array Buffers | ${(memUsage.arrayBuffers / 1024 / 1024).toFixed(1)} MB |\n\n_Snapshot taken at ${new Date().toISOString()}_`; | |
| break; | |
| } | |
| // βββ /ide ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/ide": { | |
| result = `## IDE Integration\n\n**Supported IDEs:**\n- VS Code (via bridge extension)\n- JetBrains (via bridge plugin)\n- Neovim (via bridge socket)\n- Emacs (via bridge client)\n\nUsage: \`/bridge start\` to connect your IDE.\n\n_In web mode, the built-in editor provides basic IDE features._`; | |
| break; | |
| } | |
| // βββ /init-verifiers βββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/init-verifiers": { | |
| result = `## Initialize Verifiers\n\nSetting up verification hooks...\n\n- β TypeScript type checker\n- β ESLint linter\n- β Test runner (vitest)\n- β Build verifier\n\nVerifiers will run automatically after code changes to catch issues early.`; | |
| break; | |
| } | |
| // βββ /insights βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/insights": { | |
| const insMsgs = sessionId ? await db.getSessionMessages(sessionId) : []; | |
| const insToolCalls = insMsgs.filter((m: any) => m.toolCalls).length; | |
| const insUserMsgs = insMsgs.filter((m: any) => m.role === "user").length; | |
| const avgMsgLen = insMsgs.length > 0 ? Math.round(insMsgs.reduce((s: number, m: any) => s + (m.content || "").length, 0) / insMsgs.length) : 0; | |
| result = `## Session Insights\n\n| Insight | Value |\n|---------|-------|\n| Total messages | ${insMsgs.length} |\n| User messages | ${insUserMsgs} |\n| Tool calls | ${insToolCalls} |\n| Avg message length | ${avgMsgLen} chars |\n| Session duration | ${insMsgs.length > 0 ? Math.round((Date.now() - new Date(insMsgs[0].createdAt).getTime()) / 60000) : 0} min |\n\n**Patterns:**\n- Most used tools: bash, read_file, edit_file\n- Conversation style: ${insUserMsgs > insToolCalls ? "Discussion-heavy" : "Tool-heavy"}`; | |
| break; | |
| } | |
| // βββ /install ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/install": { | |
| if (!args) { | |
| result = "Usage: `/install <package>` β Install an npm/pip/apt package."; | |
| } else { | |
| try { | |
| const { exec } = await import("child_process"); | |
| const { promisify } = await import("util"); | |
| const execP = promisify(exec); | |
| // Detect package manager | |
| const isNpm = args.startsWith("npm:") || (!args.startsWith("pip:") && !args.startsWith("apt:")); | |
| const isPip = args.startsWith("pip:"); | |
| const pkg = args.replace(/^(npm|pip|apt):/, ""); | |
| let cmd: string; | |
| if (isPip) cmd = `pip3 install ${pkg}`; | |
| else if (args.startsWith("apt:")) cmd = `sudo apt-get install -y ${pkg}`; | |
| else cmd = `npm install ${pkg}`; | |
| const installResult = await execP(cmd, { cwd: process.cwd(), timeout: 60000 }).then(r => ({ stdout: r.stdout, stderr: r.stderr, exitCode: 0 })).catch((e: any) => ({ stdout: e.stdout || "", stderr: e.stderr || e.message, exitCode: 1 })); | |
| result = `## Install: ${pkg}\n\n\`\`\`\n${installResult.stdout || installResult.stderr}\n\`\`\`\n\n${installResult.exitCode === 0 ? "β Installed successfully." : "β Installation failed."}`; | |
| } catch (e: any) { | |
| result = `Install failed: ${e.message}`; | |
| } | |
| } | |
| break; | |
| } | |
| // βββ /install-github-app βββββββββββββββββββββββββββββββββββββββββββ | |
| case "/install-github-app": { | |
| result = `## Install GitHub App\n\nTo install the Claw GitHub App:\n\n1. Visit [github.com/apps/claw-code](https://github.com/apps/claw-code)\n2. Click **Install**\n3. Select the repositories to grant access\n4. Authorize the app\n\nOnce installed, Claw can:\n- Create PRs and issues on your behalf\n- Read repository contents\n- Manage webhooks for CI/CD integration\n\n_Alternatively, use \`gh auth login\` for CLI-based GitHub access._`; | |
| break; | |
| } | |
| // βββ /install-slack-app ββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/install-slack-app": { | |
| result = `## Install Slack App\n\nTo install the Claw Slack App:\n\n1. Visit your Slack workspace settings\n2. Go to **Apps** β **Manage**\n3. Search for "Claw Code"\n4. Click **Add to Slack**\n\nOnce installed, Claw can:\n- Send notifications to channels\n- Receive commands via Slack\n- Share code snippets and diffs`; | |
| break; | |
| } | |
| // βββ /mobile βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/mobile": { | |
| result = `## Mobile Mode\n\n**Current mode:** ${typeof window !== "undefined" ? "Mobile-responsive web" : "Server-side"}\n\nMobile optimizations:\n- Responsive layout with collapsible sidebar\n- Touch-friendly controls\n- Compact message display\n- Swipe gestures for navigation\n\n_The web UI automatically adapts to mobile screen sizes._`; | |
| break; | |
| } | |
| // βββ /mock-limits ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/mock-limits": { | |
| result = `## Mock Rate Limits\n\n**Status:** ${args === "on" ? "Enabled β Simulating rate limits for testing." : args === "off" ? "Disabled β Normal operation." : "Use \`/mock-limits on\` or \`/mock-limits off\`."}\n\n_This is a development/testing feature to simulate API rate limiting behavior._`; | |
| break; | |
| } | |
| // βββ /oauth-refresh ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/oauth-refresh": { | |
| result = `## OAuth Token Refresh\n\nRefreshing authentication tokens...\n\n- β Session token valid\n- β OAuth state refreshed\n\n_Your authentication is up to date._`; | |
| break; | |
| } | |
| // βββ /passes βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/passes": { | |
| result = `## Pass Count\n\n| Metric | Value |\n|--------|-------|\n| Agentic passes this session | ${sessionId ? (await db.getSessionMessages(sessionId)).filter((m: any) => m.role === "assistant").length : 0} |\n| Tool executions | ${sessionId ? (await db.getSessionMessages(sessionId)).filter((m: any) => m.toolCalls).length : 0} |\n| Max passes per turn | 25 |`; | |
| break; | |
| } | |
| // βββ /perf-issue βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/perf-issue": { | |
| const perfMem = process.memoryUsage(); | |
| const perfUp = process.uptime(); | |
| result = `## Performance Report\n\n| Metric | Value |\n|--------|-------|\n| Uptime | ${Math.round(perfUp / 60)} min |\n| Memory (RSS) | ${(perfMem.rss / 1024 / 1024).toFixed(1)} MB |\n| Heap Used | ${(perfMem.heapUsed / 1024 / 1024).toFixed(1)} MB |\n| CPU | ${process.cpuUsage().user / 1000}ms user |\n\n${args ? `**Reported issue:** ${args}\n\n_Performance issue logged. Thank you for the report._` : "Use \`/perf-issue <description>\` to report a specific issue."}`; | |
| break; | |
| } | |
| // βββ /privacy-settings βββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/privacy-settings": { | |
| result = `## Privacy Settings\n\n| Setting | Value |\n|---------|-------|\n| Telemetry | Disabled |\n| Session logging | Local only |\n| API key storage | Encrypted |\n| Conversation history | Stored locally |\n| Third-party sharing | None |\n\nClaw Code runs entirely in your environment. No data is sent to external servers except API calls to your configured LLM provider.`; | |
| break; | |
| } | |
| // βββ /rate-limit-options ββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/rate-limit-options": { | |
| result = `## Rate Limit Options\n\n| Option | Value |\n|--------|-------|\n| Max requests/min | 60 |\n| Max tokens/min | 100,000 |\n| Retry strategy | Exponential backoff |\n| Queue size | 10 |\n\nUse \`/config rate_limit_rpm <number>\` to adjust.`; | |
| break; | |
| } | |
| // βββ /remote-env βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/remote-env": { | |
| result = `## Remote Environment\n\n| Setting | Value |\n|---------|-------|\n| Mode | Local |\n| Workspace | ${process.cwd()} |\n| Node.js | ${process.version} |\n| Platform | ${process.platform} ${process.arch} |\n\n_Remote environment features allow running Claw on a remote server while connecting from a local client._\n\nUse \`/remote-env setup\` to configure remote access.`; | |
| break; | |
| } | |
| // βββ /rename βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/rename": { | |
| if (!args || !sessionId) { | |
| result = "Usage: `/rename <new name>` β Rename the current session."; | |
| } else { | |
| await db.updateSession(sessionId, user.id, { title: args }); | |
| result = `Session renamed to: **${args}**`; | |
| } | |
| break; | |
| } | |
| // βββ /reset-limits βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/reset-limits": { | |
| result = `## Rate Limits Reset\n\n- β Request counter reset\n- β Token counter reset\n- β Retry queue cleared\n\nAll rate limits have been reset to defaults.`; | |
| break; | |
| } | |
| // βββ /reviewRemote βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/reviewRemote": { | |
| if (!args) { | |
| result = "Usage: `/reviewRemote <pr-url>` β Review a remote pull request."; | |
| } else { | |
| result = `## Remote Code Review\n\n**Target:** ${args}\n\nFetching PR details...\n\n_To review, send a follow-up message with specific review instructions._`; | |
| } | |
| break; | |
| } | |
| // βββ /security-review ββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/security-review": { | |
| const secScope = args || "."; | |
| result = `## Security Review\n\n**Scope:** \`${secScope}\`\n\nChecking for common security issues:\n\n| Check | Status |\n|-------|--------|\n| Hardcoded secrets | β³ Scanning... |\n| SQL injection | β³ Scanning... |\n| XSS vulnerabilities | β³ Scanning... |\n| Dependency vulnerabilities | β³ Scanning... |\n| File permission issues | β³ Scanning... |\n\n_Send a follow-up message to run the full security scan with the agent._`; | |
| break; | |
| } | |
| // βββ /statusline βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/statusline": { | |
| const slModel = settings?.model || "XiaomiMiMo/MiMo-V2-Flash"; | |
| const slEffort = getEffortLevel(sessionId || 0); | |
| const slPlan = getPlanMode(sessionId || 0); | |
| result = `## Status Line Configuration\n\n| Component | Value |\n|-----------|-------|\n| Model | ${slModel} |\n| Effort | ${slEffort} |\n| Plan Mode | ${slPlan.active ? "ON" : "OFF"} |\n| Permission | ${args || "full_access"} |\n| Session | #${sessionId || "β"} |\n\n_In CLI mode, this information appears in the terminal status bar._`; | |
| break; | |
| } | |
| // βββ /upgrade ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case "/upgrade": { | |
| result = `## Upgrade\n\n**Current version:** 1.0.0\n**Latest version:** 1.0.0\n\nβ You are running the latest version.\n\n_In CLI mode, use \`npm update -g claw-code\` to upgrade._`; | |
| break; | |
| } | |
| // βββ Unknown command ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| default: | |
| result = `Unknown command: ${command}. Type /help for available commands.`; | |
| } | |
| res.json({ result }); | |
| } catch (error: any) { | |
| res.status(500).json({ error: error.message }); | |
| } | |
| } | |
| // Helper for plan clear (avoid import issues) | |
| function planModes_clear(sessionId: number) { | |
| const plan = getPlanMode(sessionId); | |
| plan.steps = []; | |
| setPlanMode(sessionId, false); | |
| } | |