claw-web-v2 / server /runtime /chat-endpoint.ts
Claw Web
fix: ROOT CAUSE of agent infinite loops
38797d2
/**
* 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);
}