Skill: enderfga/openclaw-claude-code (version 0.3.0)

--- name: claude-code-skill description: Control Claude Code via MCP protocol. Execute commands, read/write files, search code, and use all Claude Code tools programmatically with agent team support. homepage: https://github.com/enderfga/claude-code-skill metadata: { "clawdis": { "emoji": "🤖", "requires": { "bins": ["node"], "env": [] }, "install": [ { "id": "local", "kind": "local", "path": "~/clawd/claude-code-skill", "label": "Use local installation" } ] } } --- # Claude Code Skill Control Claude Code via MCP (Model Context Protocol). This skill unleashes the full power of Claude Code for openclaw agents, including persistent sessions, agent teams, and advanced tool control. ## ⚡ Quick Start ```bash # Start a persistent Claude session for your project claude-code-skill session-start myproject -d ~/project \ --permission-mode plan \ --allowed-tools "Bash,Read,Edit,Write,Glob,Grep" \ --max-budget 2.00 # Send a complex task (Claude will autonomously use tools) claude-code-skill session-send myproject "Find all TODO comments and create GitHub issues" --stream # Check progress claude-code-skill session-status myproject ``` ## 🎯 When to Use This Skill ### Use Persistent Sessions When: - ✅ Multi-step tasks requiring multiple tool calls - ✅ Iterative development (write code → test → fix → repeat) - ✅ Long conversations needing full context - ✅ Agent needs to work autonomously - ✅ You want streaming real-time feedback ### Use Direct MCP Tools When: - ✅ Single command execution - ✅ Quick file read/write - ✅ One-off searches - ✅ No context needed between operations ## 📚 Command Reference ### Basic MCP Operations ```bash # Connect to Claude Code MCP claude-code-skill connect claude-code-skill status claude-code-skill tools # Direct tool calls (no persistent session) claude-code-skill bash "npm test" claude-code-skill read /path/to/file.ts claude-code-skill glob "**/*.ts" -p ~/project claude-code-skill grep "TODO" -p ~/project -c claude-code-skill call Write -a '{"file_path":"/tmp/test.txt","content":"Hello"}' # Disconnect claude-code-skill disconnect ``` ### Persistent Sessions (Agent Loop) #### Starting Sessions ```bash # Basic start claude-code-skill session-start myproject -d ~/project # With custom API endpoint (for Gemini/GPT proxy) claude-code-skill session-start gemini-task -d ~/project \ --base-url http://127.0.0.1:8082 \ --model gemini-2.0-flash # With permission mode (plan = preview changes before applying) claude-code-skill session-start review -d ~/project --permission-mode plan # With tool whitelist (auto-approve these tools) claude-code-skill session-start safe -d ~/project \ --allowed-tools "Bash(git:*),Read,Glob,Grep" # With budget limit claude-code-skill session-start limited -d ~/project --max-budget 1.50 # Full configuration claude-code-skill session-start advanced -d ~/project \ --permission-mode acceptEdits \ --allowed-tools "Bash,Read,Edit,Write" \ --disallowed-tools "Task" \ --max-budget 5.00 \ --model claude-opus-4-5 \ --append-system-prompt "Always write tests" \ --add-dir "/tmp,/var/log" ``` **Permission Modes:** | Mode | Description | |------|-------------| | `acceptEdits` | Auto-accept file edits (default) | | `plan` | Preview changes before applying | | `default` | Ask for each operation | | `bypassPermissions` | Skip all prompts (dangerous!) | | `delegate` | Delegate decisions to parent | | `dontAsk` | Never ask, reject by default | #### Sending Messages ```bash # Basic send (blocks until complete) claude-code-skill session-send myproject "Write unit tests for auth.ts" # Streaming (see progress in real-time) claude-code-skill session-send myproject "Refactor this module" --stream # With custom timeout claude-code-skill session-send myproject "Run all tests" -t 300000 ``` #### Managing Sessions ```bash # List active sessions claude-code-skill session-list # Get detailed status claude-code-skill session-status myproject # View conversation history claude-code-skill session-history myproject -n 50 # Pause and resume claude-code-skill session-pause myproject claude-code-skill session-resume-paused myproject # Fork a session (create a branch for experiments) claude-code-skill session-fork myproject myproject-experiment # Stop claude-code-skill session-stop myproject # Restart a failed session claude-code-skill session-restart myproject ``` ### Session History & Search ```bash # Browse all Claude Code sessions claude-code-skill sessions -n 20 # Search sessions by project claude-code-skill session-search --project ~/myapp # Search by time claude-code-skill session-search --since "2h" claude-code-skill session-search --since "2024-02-01" # Search by query claude-code-skill session-search "bug fix" # Resume a historical session claude-code-skill resume <session-id> "Continue where we left off" -d ~/project ``` ### Batch Operations ```bash # Read multiple files at once claude-code-skill batch-read "src/**/*.ts" "tests/**/*.test.ts" -p ~/project ``` ## 🤝 Agent Team Features Deploy multiple Claude agents working together on complex tasks. ### Basic Agent Team ```bash # Define a team of agents claude-code-skill session-start team-project -d ~/project \ --agents '{ "architect": { "description": "Designs system architecture", "prompt": "You are a senior software architect. Design scalable, maintainable systems." }, "developer": { "description": "Implements features", "prompt": "You are a full-stack developer. Write clean, tested code." }, "reviewer": { "description": "Reviews code quality", "prompt": "You are a code reviewer. Check for bugs, style issues, and improvements." } }' \ --agent architect # Switch between agents mid-conversation claude-code-skill session-send team-project "Design the authentication system" # (architect responds) claude-code-skill session-send team-project "@developer implement the design" # (developer agent takes over) claude-code-skill session-send team-project "@reviewer review the implementation" # (reviewer agent takes over) ``` ### Pre-configured Team Templates ```bash # Code review team claude-code-skill session-start review -d ~/project \ --agents '{ "security": {"prompt": "Focus on security vulnerabilities"}, "performance": {"prompt": "Focus on performance issues"}, "quality": {"prompt": "Focus on code quality and maintainability"} }' \ --agent security # Full-stack team claude-code-skill session-start fullstack -d ~/project \ --agents '{ "frontend": {"prompt": "React/TypeScript frontend specialist"}, "backend": {"prompt": "Node.js/Express backend specialist"}, "database": {"prompt": "PostgreSQL/Redis database specialist"} }' \ --agent frontend ``` ## 🔧 Advanced Features ### Tool Control ```bash # Allow specific tools with patterns --allowed-tools "Bash(git:*,npm:*),Read,Edit" # Deny dangerous operations --disallowed-tools "Bash(rm:*,sudo:*),Write(/etc/*)" # Limit to specific tool set --tools "Read,Glob,Grep" # Disable all tools --tools "" ``` ### System Prompts ```bash # Replace system prompt completely --system-prompt "You are a Python expert. Always use type hints." # Append to existing prompt --append-system-prompt "Always run tests after changes." ``` ### Session Management ```bash # Resume with fork (create a branch) --resume <session-id> --fork-session # Use custom UUID for session --session-id [REDACTED_SECRET] # Add additional working directories --add-dir "/var/log,/tmp/workspace" ``` ### Multi-Model Support (Proxy) Use `--base-url` to route requests through a proxy, enabling other models (Gemini, GPT) to power Claude Code: ```bash # Use Gemini via claude-code-proxy claude-code-skill session-start gemini-task -d ~/project \ --base-url http://127.0.0.1:8082 \ --model claude-3-5-sonnet-20241022 # Proxy will map to Gemini # Use GPT via proxy claude-code-skill session-start gpt-task -d ~/project \ --base-url http://127.0.0.1:8082 \ --model claude-3-haiku-20240307 # Proxy will map to GPT ``` **Note:** Requires `claude-code-proxy` running on port 8082 with proper API keys configured. ```bash # Start the proxy cd ~/clawd/claude-code-proxy && source .venv/bin/activate uvicorn server:app --host 127.0.0.1 --port 8082 ``` ## 🎓 Best Practices ### For OpenClaw Agents 1. **Always use persistent sessions for multi-step tasks** ```bash # ❌ Bad: Multiple disconnect/reconnect cycles claude-code-skill bash "step1" claude-code-skill bash "step2" # ✅ Good: Single persistent session claude-code-skill session-start task -d ~/project claude-code-skill session-send task "Do step1 then step2" ``` 2. **Use `--stream` for long-running tasks** ```bash claude-code-skill session-send task "Run full test suite" --stream ``` 3. **Set budget limits for safety** ```bash --max-budget 2.00 # Stop after $2 of API usage ``` 4. **Use plan mode for critical changes** ```bash --permission-mode plan # Preview before applying ``` 5. **Fork before experiments** ```bash claude-code-skill session-fork main experimental claude-code-skill session-send experimental "Try risky refactor" ``` ### Error Recovery ```bash # If session fails: claude-code-skill session-status myproject # Check what happened claude-code-skill session-history myproject -n 20 # See recent events claude-code-skill session-restart myproject # Restart from last good state # If you need to start over: claude-code-skill session-stop myproject claude-code-skill session-start myproject -d ~/project --resume <old-session-id> ``` ## 🏗️ Architecture ``` openclaw agent ↓ claude-code-skill CLI (this tool) ↓ HTTP sasha-doctor API (:18795) ↓ MCP claude mcp serve (Claude Code) ↓ Your files & tools ``` ## 🔌 Available Tools (via MCP) All Claude Code tools are accessible: | Tool | Description | |------|-------------| | Bash | Execute shell commands | | Read | Read file contents | | Write | Create/overwrite files | | Edit | Edit files with string replacement | | Glob | Find files by pattern | | Grep | Search file contents | | Task | Launch sub-agents | | WebFetch | Fetch web content | | WebSearch | Search the web | | Git* | Git operations | | AskUserQuestion | Interactive prompts | | ... | and 10+ more | ## 📊 Examples ### Example 1: Code Review ```bash claude-code-skill session-start review -d ~/myapp \ --permission-mode plan \ --agents '{"security":{"prompt":"Focus on security"},"quality":{"prompt":"Focus on quality"}}' \ --agent security claude-code-skill session-send review \ "Review all TypeScript files in src/, check for security issues and code quality problems" \ --stream ``` ### Example 2: Automated Testing ```bash claude-code-skill session-start test -d ~/myapp \ --allowed-tools "Bash(npm:*,git:*),Read,Write" \ --max-budget 1.00 claude-code-skill session-send test \ "Find all untested functions, write unit tests, run tests, fix failures" ``` ### Example 3: Multi-Agent Debugging ```bash claude-code-skill session-start debug -d ~/myapp \ --agents '{ "detective": {"prompt": "Find the root cause of bugs"}, "fixer": {"prompt": "Implement fixes"}, "tester": {"prompt": "Verify fixes work"} }' \ --agent detective claude-code-skill session-send debug "We have a memory leak in the API server" --stream # Detective investigates, then hands off to fixer, then to tester ``` ## 🔗 Integration with OpenClaw When openclaw needs to perform complex coding tasks: ```bash # From within openclaw agent context: openclaw skills run claude-code-skill -- session-start task -d ~/project openclaw skills run claude-code-skill -- session-send task "Implement feature X" --stream openclaw skills run claude-code-skill -- session-status task ``` Or use the skill programmatically via sasha-doctor HTTP API (see TOOLS.md section 3). ## 📖 See Also - **TOOLS.md section 3** - Full HTTP API documentation - **sasha-doctor endpoints** - Backend integration details - **Claude Code docs** - Official Claude Code documentation (query via `qmd` tool)

path: README.md
content: # Claude Code Skill 🤖

Control Claude Code via MCP (Model Context Protocol). This CLI provides programmatic access to Claude Code's full capabilities including persistent sessions, agent teams, and advanced tool control.

## Features

- 🔌 **MCP Protocol** - Direct access to all Claude Code tools
- 💾 **Persistent Sessions** - Maintain context across multiple interactions
- 🤝 **Agent Teams** - Deploy multiple specialized agents
- 🔧 **Tool Control** - Fine-grained control over which tools are available
- 📊 **Budget Limits** - Set spending caps on API usage
- 🔄 **Session Management** - Fork, pause, resume, search sessions

## Installation

```bash
# Clone
git clone https://github.com/Enderfga/openclaw-claude-code-skill.git
cd openclaw-claude-code-skill

# Install dependencies
npm install

# Build
npm run build

# Link globally (optional)
npm link
```

## Requirements

- Node.js 18+
- Backend API server running (see Configuration)
- Claude Code CLI (for the backend)

## Multi-Model Support (Proxy) 🌐

Use `--base-url` to route requests through a custom API endpoint, enabling **any OpenAI-compatible model** to power Claude Code:

```bash
# Use Gemini via claude-code-proxy
claude-code-skill session-start gemini-task -d ~/project \
  --model gemini-2.0-flash \
  --base-url http://127.0.0.1:8082

# Use GPT-4o via OpenAI-compatible endpoint
claude-code-skill session-start gpt-task -d ~/project \
  --model gpt-4o \
  --base-url https://api.openai.com/v1

# Use any OpenRouter model
claude-code-skill session-start mixtral-task -d ~/project \
  --model mistral/mixtral-8x7b \
  --base-url https://openrouter.ai/api/v1
```

This unlocks the full Claude Code agent loop (persistent sessions, tool use, multi-turn) with any model backend.

## Quick Start

```bash
# Start a persistent session
claude-code-skill session-start myproject -d ~/project \
  --permission-mode plan \
  --allowed-tools "Bash,Read,Edit,Write,Glob,Grep"

# Send a task
claude-code-skill session-send myproject "Find all TODO comments and fix them" --stream

# Check status
claude-code-skill session-status myproject

# Stop when done
claude-code-skill session-stop myproject
```

## Configuration

The CLI connects to a backend API server. Set the URL via environment variable:

```bash
# Default: http://127.0.0.1:18795
export SASHA_DOCTOR_URL="http://your-server:port"
# Alias also supported:
export CLAUDE_CODE_API_URL="http://your-server:port"
```

## Basic Commands

```bash
# Connection
claude-code-skill connect          # Connect to MCP server
claude-code-skill disconnect       # Disconnect
claude-code-skill status           # Check connection status
claude-code-skill tools            # List available tools

# Direct tool calls
claude-code-skill bash "npm test"
claude-code-skill read /path/to/file.ts
claude-code-skill glob "**/*.ts" -p ~/project
claude-code-skill grep "TODO" -p ~/project -c
claude-code-skill call Write -a '{"file_path":"/tmp/test.txt","content":"Hello"}'
```

## Persistent Sessions

### Start a Session

```bash
# Basic
claude-code-skill session-start myproject -d ~/project

# With custom model and API endpoint (proxy support)
claude-code-skill session-start gemini-dev -d ~/project \
  --model gemini-2.0-flash \
  --base-url http://127.0.0.1:8082

# With full options
claude-code-skill session-start advanced -d ~/project \
  --model claude-opus-4-5 \
  --permission-mode plan \
  --allowed-tools "Bash,Read,Edit,Write" \
  --max-budget 2.00 \
  --append-system-prompt "Always write tests"
```

| Option | Description |
|--------|-------------|
| `-m, --model <model>` | Model to use (e.g., `claude-opus-4-5`, `gemini-2.0-flash`) |
| `-b, --base-url <url>` | Custom API endpoint for proxy/alternative backends |

### Permission Modes

| Mode | Description |
|------|-------------|
| `acceptEdits` | Auto-accept file edits (default) |
| `plan` | Preview changes before applying |
| `default` | Ask for each operation |
| `bypassPermissions` | Skip all prompts (dangerous!) |

### Session Management

```bash
claude-code-skill session-list                    # List active sessions
claude-code-skill session-send myproject "task"   # Send message
claude-code-skill session-send myproject "task" --stream  # With streaming
claude-code-skill session-status myproject        # Get status
claude-code-skill session-history myproject -n 50 # View history
claude-code-skill session-pause myproject         # Pause session
claude-code-skill session-resume-paused myproject # Resume session
claude-code-skill session-fork myproject exp      # Fork session
claude-code-skill session-stop myproject          # Stop session
claude-code-skill session-restart myproject       # Restart failed session
```

## Agent Teams

Deploy multiple specialized agents:

```bash
claude-code-skill session-start team -d ~/project \
  --agents '{
    "architect": {"prompt": "Design system architecture"},
    "developer": {"prompt": "Implement features"},
    "reviewer": {"prompt": "Review code quality"}
  }' \
  --agent architect

# Switch agents with @mention
claude-code-skill session-send team "@developer implement the design"
claude-code-skill session-send team "@reviewer review the implementation"
```

## Tool Control

```bash
# Allow specific tools
--allowed-tools "Bash(git:*,npm:*),Read,Edit"

# Deny dangerous operations
--disallowed-tools "Bash(rm:*,sudo:*),Write(/etc/*)"

# Limit available tools
--tools "Read,Glob,Grep"
```

## Session Search

```bash
claude-code-skill sessions -n 20              # List recent sessions
claude-code-skill session-search "bug fix"    # Search by query
claude-code-skill session-search --project ~/myapp  # Filter by project
claude-code-skill session-search --since "2h" # Filter by time
claude-code-skill resume <session-id> "Continue" -d ~/project  # Resume
```

## Batch Operations

```bash
# Read multiple files
claude-code-skill batch-read "src/**/*.ts" "tests/**/*.test.ts" -p ~/project
```

## Examples

### Code Review

```bash
claude-code-skill session-start review -d ~/project --permission-mode plan
claude-code-skill session-send review "Review all TypeScript files for security issues" --stream
```

### Automated Testing

```bash
claude-code-skill session-start test -d ~/project \
  --allowed-tools "Bash(npm:*),Read,Write" \
  --max-budget 1.00

claude-code-skill session-send test "Find untested functions and write tests"
```

### Multi-Agent Debugging

```bash
claude-code-skill session-start debug -d ~/project \
  --agents '{
    "detective": {"prompt": "Find root cause"},
    "fixer": {"prompt": "Implement fixes"},
    "tester": {"prompt": "Verify fixes"}
  }' \
  --agent detective

claude-code-skill session-send debug "Memory leak in API server" --stream
```

## Best Practices

1. **Use persistent sessions** for multi-step tasks
2. **Enable streaming** (`--stream`) for long operations
3. **Set budget limits** (`--max-budget`) for safety
4. **Use plan mode** (`--permission-mode plan`) for critical changes
5. **Fork before experiments** to preserve the original session

## License

MIT


path: examples/basic-mcp.ts
content: /**
 * Basic MCP Setup Example
 *
 * This example shows how to initialize and use MCP servers
 * with the openclaw-claude-code-skill package.
 */

import {
  initializeMcpSystem,
  addMcpServer,
  executeMcpAction,
  getAllTools,
  getClientsStatus,
  setConfigPath,
} from "../src";

async function main() {
  // Optional: Set a custom config path
  setConfigPath("./mcp_config.json");

  // Initialize the MCP system
  console.log("Initializing MCP system...");
  await initializeMcpSystem();

  // Add a filesystem server
  console.log("Adding filesystem server...");
  await addMcpServer("filesystem", {
    command: "npx",
    args: ["-y", "@modelcontextprotocol/server-filesystem", "./"],
  });

  // Wait for initialization
  await new Promise((resolve) => setTimeout(resolve, 2000));

  // Check status
  const status = await getClientsStatus();
  console.log("Server status:", status);

  // Get all available tools
  const tools = await getAllTools();
  console.log("Available tools:", JSON.stringify(tools, null, 2));

  // Execute a tool call (list directory)
  try {
    const result = await executeMcpAction("filesystem", {
      method: "tools/call",
      params: {
        name: "list_directory",
        arguments: { path: "./" },
      },
    });
    console.log("Directory listing:", result);
  } catch (error) {
    console.error("Error executing tool:", error);
  }
}

main().catch(console.error);


path: examples/chat-store.ts
content: /**
 * Persistent Chat Store Example
 *
 * This example shows how to create a persistent chat store
 * with automatic context recovery.
 */

import {
  createPersistStore,
  indexedDBStorage,
  mergeSessions,
  Session,
} from "../src";

// Define chat message type
interface ChatMessage {
  id: string;
  role: "user" | "assistant" | "system";
  content: string;
  date: Date;
}

// Define chat session type
interface ChatSession extends Session {
  id: string;
  topic: string;
  messages: ChatMessage[];
  lastUpdate: Date;
}

// Define store state
interface ChatState {
  sessions: ChatSession[];
  currentSessionId: string | null;
}

// Create the persistent chat store
const useChatStore = createPersistStore<
  ChatState,
  {
    createSession: (topic?: string) => string;
    deleteSession: (id: string) => void;
    addMessage: (sessionId: string, message: Omit<ChatMessage, "id" | "date">) => void;
    setCurrentSession: (id: string) => void;
    syncWithRemote: (remoteSessions: ChatSession[]) => void;
  }
>(
  // Initial state
  {
    sessions: [],
    currentSessionId: null,
  },
  // Methods
  (set, get) => ({
    createSession: (topic = "New Chat") => {
      const id = `session-${Date.now()}`;
      const newSession: ChatSession = {
        id,
        topic,
        messages: [],
        lastUpdate: new Date(),
      };
      set({
        sessions: [newSession, ...get().sessions],
        currentSessionId: id,
      });
      return id;
    },

    deleteSession: (id: string) => {
      const { sessions, currentSessionId } = get();
      const newSessions = sessions.filter((s) => s.id !== id);
      set({
        sessions: newSessions,
        currentSessionId:
          currentSessionId === id
            ? newSessions[0]?.id ?? null
            : currentSessionId,
      });
    },

    addMessage: (sessionId: string, message: Omit<ChatMessage, "id" | "date">) => {
      const sessions = get().sessions.map((session) => {
        if (session.id !== sessionId) return session;
        return {
          ...session,
          messages: [
            ...session.messages,
            {
              ...message,
              id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
              date: new Date(),
            },
          ],
          lastUpdate: new Date(),
        };
      });
      set({ sessions });
    },

    setCurrentSession: (id: string) => {
      set({ currentSessionId: id });
    },

    syncWithRemote: (remoteSessions: ChatSession[]) => {
      const { sessions } = get();
      const mergedSessions = mergeSessions(
        sessions,
        remoteSessions,
      ) as ChatSession[];
      set({ sessions: mergedSessions });
    },
  }),
  // Persist options
  {
    name: "chat-store",
    version: 1,
  },
  // Use IndexedDB storage
  indexedDBStorage,
);

// Usage example
async function main() {
  const store = useChatStore.getState();

  // Wait for hydration
  if (!store._hasHydrated) {
    console.log("Waiting for state to hydrate...");
    await new Promise<void>((resolve) => {
      const unsubscribe = useChatStore.subscribe((state) => {
        if (state._hasHydrated) {
          unsubscribe();
          resolve();
        }
      });
    });
  }

  console.log("State hydrated! Existing sessions:", store.sessions.length);

  // Create a new session
  const sessionId = store.createSession("Test Chat");
  console.log("Created session:", sessionId);

  // Add a message
  store.addMessage(sessionId, {
    role: "user",
    content: "Hello, world!",
  });

  console.log("Current sessions:", useChatStore.getState().sessions);
}

main().catch(console.error);


path: mcp_config.example.json
content: {
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/directory"],
      "status": "active"
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "your-github-token-here"
      },
      "status": "active"
    },
    "slack": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-slack"],
      "env": {
        "SLACK_BOT_TOKEN": "your-slack-bot-token",
        "SLACK_TEAM_ID": "your-team-id"
      },
      "status": "paused"
    }
  }
}


path: package.json
content: {
  "name": "claude-code-skill",
  "version": "1.0.0",
  "description": "ClawdBot skill to control Claude Code via MCP",
  "type": "module",
  "bin": {
    "claude-code-skill": "./dist/index.js"
  },
  "main": "./dist/index.js",
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "keywords": ["claude", "claude-code", "mcp", "openclaw", "skill"],
  "author": "enderfga",
  "license": "MIT",
  "dependencies": {
    "commander": "^12.1.0",
    "node-fetch": "^3.3.2"
  },
  "devDependencies": {
    "@types/node": "^22.10.0",
    "tsx": "^4.19.0",
    "typescript": "^5.7.0"
  }
}


path: src/index.ts
content: #!/usr/bin/env node
/**
 * Claude Code Skill for ClawdBot
 * Provides CLI access to Claude Code via MCP protocol
 */

import { Command } from 'commander';

const SASHA_DOCTOR_URL = process.env.SASHA_DOCTOR_URL || 'http://127.0.0.1:18795';
const PREFIX = '/sasha-doctor/claude-code';

interface ApiResponse {
  ok: boolean;
  error?: string;
  [key: string]: unknown;
}

async function apiCall(endpoint: string, method: string = 'GET', body?: object): Promise<ApiResponse> {
  const url = `${SASHA_DOCTOR_URL}${PREFIX}${endpoint}`;
  const options: RequestInit = {
    method,
    headers: { 'Content-Type': 'application/json' },
  };
  if (body) {
    options.body = JSON.stringify(body);
  }

  try {
    const response = await fetch(url, options);
    return await response.json() as ApiResponse;
  } catch (error) {
    return { ok: false, error: (error as Error).message };
  }
}

const program = new Command();

program
  .name('claude-code-skill')
  .description('Control Claude Code via MCP protocol')
  .version('1.0.0');

// Connect command
program
  .command('connect')
  .description('Connect to Claude Code MCP server')
  .action(async () => {
    console.log('Connecting to Claude Code...');
    const result = await apiCall('/connect', 'POST');
    if (result.ok) {
      console.log(`Connected! Status: ${result.status}`);
      console.log(`Server: ${JSON.stringify(result.server)}`);
      console.log(`Available tools: ${result.tools}`);
    } else {
      console.error(`Failed: ${result.error}`);
      process.exit(1);
    }
  });

// Disconnect command
program
  .command('disconnect')
  .description('Disconnect from Claude Code MCP server')
  .action(async () => {
    const result = await apiCall('/disconnect', 'POST');
    if (result.ok) {
      console.log('Disconnected from Claude Code');
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// List tools command
program
  .command('tools')
  .description('List available Claude Code tools')
  .action(async () => {
    const result = await apiCall('/tools');
    if (result.ok && Array.isArray(result.tools)) {
      console.log('Available tools:');
      for (const tool of result.tools) {
        const t = tool as { name: string; description: string };
        console.log(`  - ${t.name}: ${t.description}`);
      }
    } else {
      console.error(`Failed: ${result.error || 'Not connected'}`);
      console.log('Tip: Run "claude-code-skill connect" first');
    }
  });

// Bash command
program
  .command('bash <command>')
  .description('Execute a bash command via Claude Code')
  .option('-d, --description <desc>', 'Description of what the command does')
  .action(async (command: string, options: { description?: string }) => {
    const result = await apiCall('/bash', 'POST', {
      command,
      description: options.description || ''
    });
    if (result.ok) {
      const r = result.result as { stdout?: string; stderr?: string };
      if (r.stdout) console.log(r.stdout);
      if (r.stderr) console.error(r.stderr);
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Read file command
program
  .command('read <file>')
  .description('Read a file via Claude Code')
  .action(async (filePath: string) => {
    const result = await apiCall('/read', 'POST', { file_path: filePath });
    if (result.ok) {
      const r = result.result as { type?: string; file?: { content?: string } };
      if (r.file?.content) {
        console.log(r.file.content);
      } else {
        console.log(JSON.stringify(result.result, null, 2));
      }
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Call any tool
program
  .command('call <tool>')
  .description('Call any Claude Code tool with JSON args')
  .option('-a, --args <json>', 'JSON arguments for the tool', '{}')
  .action(async (tool: string, options: { args: string }) => {
    let args: object;
    try {
      args = JSON.parse(options.args);
    } catch {
      console.error('Invalid JSON args');
      process.exit(1);
    }

    const result = await apiCall('/call', 'POST', { tool, args });
    if (result.ok) {
      console.log(JSON.stringify(result.result, null, 2));
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Glob search
program
  .command('glob <pattern>')
  .description('Search for files by pattern')
  .option('-p, --path <dir>', 'Directory to search in')
  .action(async (pattern: string, options: { path?: string }) => {
    const args: { pattern: string; path?: string } = { pattern };
    if (options.path) args.path = options.path;

    const result = await apiCall('/call', 'POST', { tool: 'Glob', args });
    if (result.ok) {
      const r = result.result as { filenames?: string[] };
      if (r.filenames) {
        for (const f of r.filenames) console.log(f);
      } else {
        console.log(JSON.stringify(result.result, null, 2));
      }
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Grep search
program
  .command('grep <pattern>')
  .description('Search file contents')
  .option('-p, --path <dir>', 'Directory to search in')
  .option('-g, --glob <pattern>', 'File pattern to filter')
  .option('-c, --content', 'Show matching content')
  .action(async (pattern: string, options: { path?: string; glob?: string; content?: boolean }) => {
    const args: Record<string, unknown> = { pattern };
    if (options.path) args.path = options.path;
    if (options.glob) args.glob = options.glob;
    if (options.content) args.output_mode = 'content';

    const result = await apiCall('/call', 'POST', { tool: 'Grep', args });
    if (result.ok) {
      const r = result.result as { filenames?: string[]; content?: string };
      if (r.content) {
        console.log(r.content);
      } else if (r.filenames) {
        for (const f of r.filenames) console.log(f);
      } else {
        console.log(JSON.stringify(result.result, null, 2));
      }
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Status command
program
  .command('status')
  .description('Check connection status')
  .action(async () => {
    // Try to get tools - if it works, we're connected
    const result = await apiCall('/tools');
    if (result.ok) {
      console.log('Status: Connected');
      console.log(`Tools available: ${(result.tools as unknown[]).length}`);
    } else {
      console.log('Status: Not connected');
      console.log('Run "claude-code-skill connect" to connect');
    }
  });

// List sessions command
program
  .command('sessions')
  .description('List all Claude Code sessions')
  .option('-n, --limit <n>', 'Limit number of sessions', '10')
  .action(async (options: { limit: string }) => {
    const result = await apiCall('/sessions');
    if (result.ok && Array.isArray(result.sessions)) {
      const sessions = result.sessions.slice(0, parseInt(options.limit));
      console.log('Recent Claude Code sessions:\n');
      for (const s of sessions) {
        const sess = s as {
          sessionId: string;
          summary?: string;
          projectPath?: string;
          modified?: string;
          messageCount?: number;
        };
        console.log(`  ${sess.sessionId}`);
        console.log(`    Summary: ${sess.summary || 'N/A'}`);
        console.log(`    Project: ${sess.projectPath || 'N/A'}`);
        console.log(`    Modified: ${sess.modified || 'N/A'}`);
        console.log(`    Messages: ${sess.messageCount || 0}`);
        console.log('');
      }
    } else {
      console.error(`Failed: ${result.error || 'Unknown error'}`);
    }
  });

// Resume session command
program
  .command('resume <sessionId> <prompt>')
  .description('Resume a Claude Code session with a prompt')
  .option('-d, --cwd <dir>', 'Working directory')
  .action(async (sessionId: string, prompt: string, options: { cwd?: string }) => {
    console.log(`Resuming session ${sessionId}...`);
    const result = await apiCall('/resume', 'POST', {
      sessionId,
      prompt,
      cwd: options.cwd
    });
    if (result.ok) {
      console.log(result.output as string);
      if (result.stderr) console.error(result.stderr as string);
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Continue command
program
  .command('continue <prompt>')
  .description('Continue the most recent session in a directory')
  .option('-d, --cwd <dir>', 'Working directory', process.cwd())
  .action(async (prompt: string, options: { cwd: string }) => {
    console.log(`Continuing session in ${options.cwd}...`);
    const result = await apiCall('/continue', 'POST', {
      prompt,
      cwd: options.cwd
    });
    if (result.ok) {
      console.log(result.output as string);
      if (result.stderr) console.error(result.stderr as string);
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// === Persistent Session Commands ===

// Start a persistent session
program
  .command('session-start [name]')
  .description('Start a persistent Claude Code session')
  .option('-d, --cwd <dir>', 'Working directory')
  .option('-r, --resume <sessionId>', 'Resume an existing Claude session')
  .option('-m, --model <model>', 'Model to use')
  .option('-b, --base-url <url>', 'Custom API endpoint (for Gemini/GPT proxy)')
  .option('--permission-mode <mode>', 'Permission mode: acceptEdits, bypassPermissions, default, delegate, dontAsk, plan', 'acceptEdits')
  .option('--fork-session', 'Create a new session ID instead of reusing (use with --resume)')
  .option('--allowed-tools <tools>', 'Comma-separated list of tools to auto-approve (e.g. Bash,Read,Edit)')
  .option('--disallowed-tools <tools>', 'Comma-separated list of tools to deny')
  .option('--tools <tools>', 'Limit available tools (use "" to disable all, "default" for all)')
  .option('--max-turns <n>', 'Maximum agent loop turns')
  .option('--max-budget <usd>', 'Maximum API spend in USD')
  .option('--system-prompt <prompt>', 'Replace system prompt completely')
  .option('--append-system-prompt <prompt>', 'Append to system prompt')
  .option('--skip-permissions', 'Skip all permission checks (dangerous!)')
  .option('--agents <json>', 'Custom sub-agents JSON (e.g. \'{"reviewer": {"prompt": "You review code"}}\')')
  .option('--agent <name>', 'Default agent to use')
  .option('--session-id <uuid>', 'Use a specific session ID (must be valid UUID)')
  .option('--add-dir <dirs>', 'Additional directories to allow tool access (comma-separated)')
  .action(async (name: string | undefined, options: {
    cwd?: string;
    resume?: string;
    model?: string;
    baseUrl?: string;
    permissionMode?: string;
    forkSession?: boolean;
    allowedTools?: string;
    disallowedTools?: string;
    tools?: string;
    maxTurns?: string;
    maxBudget?: string;
    systemPrompt?: string;
    appendSystemPrompt?: string;
    skipPermissions?: boolean;
    agents?: string;
    agent?: string;
    sessionId?: string;
    addDir?: string;
  }) => {
    const sessionName = name || `session-${Date.now()}`;
    console.log(`Starting persistent session: ${sessionName}...`);

    const body: Record<string, unknown> = {
      name: sessionName,
      cwd: options.cwd,
      sessionId: options.resume,
      model: options.model,
      baseUrl: options.baseUrl,
      permissionMode: options.permissionMode || 'acceptEdits',
      forkSession: options.forkSession
    };

    // New flags
    if (options.allowedTools) {
      body.allowedTools = options.allowedTools.split(',').map(t => t.trim());
    }
    if (options.disallowedTools) {
      body.disallowedTools = options.disallowedTools.split(',').map(t => t.trim());
    }
    if (options.tools) {
      body.tools = options.tools === '""' ? [] : options.tools.split(',').map(t => t.trim());
    }
    if (options.maxTurns) {
      body.maxTurns = parseInt(options.maxTurns);
    }
    if (options.maxBudget) {
      body.maxBudgetUsd = parseFloat(options.maxBudget);
    }
    if (options.systemPrompt) {
      body.systemPrompt = options.systemPrompt;
    }
    if (options.appendSystemPrompt) {
      body.appendSystemPrompt = options.appendSystemPrompt;
    }
    if (options.skipPermissions) {
      body.dangerouslySkipPermissions = true;
    }
    if (options.agents) {
      try {
        body.agents = JSON.parse(options.agents);
      } catch {
        console.error('Invalid --agents JSON');
        process.exit(1);
      }
    }
    if (options.agent) {
      body.agent = options.agent;
    }
    if (options.sessionId) {
      body.customSessionId = options.sessionId;
    }
    if (options.addDir) {
      body.addDir = options.addDir.split(',').map(d => d.trim());
    }

    const result = await apiCall('/session/start', 'POST', body);

    if (result.ok) {
      console.log(`Session '${sessionName}' started!`);
      if (result.claudeSessionId) {
        console.log(`Claude Session ID: ${result.claudeSessionId}`);
      }
      // Show active options
      if (options.model) console.log(`Model: ${options.model}`);
      if (options.baseUrl) console.log(`Base URL: ${options.baseUrl}`);
      console.log(`Permission mode: ${options.permissionMode || 'acceptEdits'}`);
      if (options.allowedTools) console.log(`Allowed tools: ${options.allowedTools}`);
      if (options.disallowedTools) console.log(`Disallowed tools: ${options.disallowedTools}`);
      if (options.tools) console.log(`Available tools: ${options.tools}`);
      if (options.maxTurns) console.log(`Max turns: ${options.maxTurns}`);
      if (options.maxBudget) console.log(`Max budget: $${options.maxBudget}`);
      if (options.forkSession) console.log(`Fork mode: enabled`);
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Send message to a persistent session
program
  .command('session-send <name> <message>')
  .description('Send a message to a persistent session')
  .option('-t, --timeout <ms>', 'Timeout in milliseconds', '120000')
  .option('-s, --stream', 'Stream output in real-time')
  .action(async (name: string, message: string, options: { timeout: string; stream?: boolean }) => {
    console.log(`Sending to session '${name}'...`);

    if (options.stream) {
      // Use SSE streaming endpoint
      const url = `${SASHA_DOCTOR_URL}${PREFIX}/session/send-stream`;
      try {
        const response = await fetch(url, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ name, message, timeout: parseInt(options.timeout) })
        });

        if (!response.ok || !response.body) {
          console.error('Failed to connect to stream');
          return;
        }

        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            if (line.startsWith('data: ')) {
              try {
                const data = JSON.parse(line.slice(6)) as { type: string; text?: string; tool?: string; error?: string };
                if (data.type === 'text') {
                  process.stdout.write(data.text || '');
                } else if (data.type === 'tool_use') {
                  console.log(`\n🔧 [Tool: ${data.tool}]`);
                } else if (data.type === 'tool_result') {
                  console.log('✓ Tool completed');
                } else if (data.type === 'done') {
                  console.log('\n--- Done ---');
                } else if (data.type === 'error') {
                  console.error(`\nError: ${data.error}`);
                }
              } catch {
                // Ignore parse errors
              }
            }
          }
        }
      } catch (error) {
        console.error(`Stream error: ${(error as Error).message}`);
      }
    } else {
      // Non-streaming mode
      const result = await apiCall('/session/send', 'POST', {
        name,
        message,
        timeout: parseInt(options.timeout)
      });

      if (result.ok) {
        console.log(result.response as string);
      } else {
        console.error(`Failed: ${result.error}`);
      }
    }
  });

// List active sessions
program
  .command('session-list')
  .description('List active persistent sessions')
  .action(async () => {
    const result = await apiCall('/session/list');
    if (result.ok && Array.isArray(result.sessions)) {
      if (result.sessions.length === 0) {
        console.log('No active persistent sessions.');
      } else {
        console.log('Active persistent sessions:\n');
        for (const s of result.sessions) {
          const sess = s as {
            name: string;
            cwd?: string;
            created?: string;
            isReady?: boolean;
          };
          console.log(`  ${sess.name}`);
          console.log(`    CWD: ${sess.cwd || 'N/A'}`);
          console.log(`    Created: ${sess.created || 'N/A'}`);
          console.log(`    Ready: ${sess.isReady ? 'Yes' : 'No'}`);
          console.log('');
        }
      }
    } else {
      console.error(`Failed: ${result.error || 'Unknown error'}`);
    }
  });

// Stop a persistent session
program
  .command('session-stop <name>')
  .description('Stop a persistent session')
  .action(async (name: string) => {
    const result = await apiCall('/session/stop', 'POST', { name });
    if (result.ok) {
      console.log(`Session '${name}' stopped.`);
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Get session status
program
  .command('session-status <name>')
  .description('Get detailed status of a persistent session')
  .action(async (name: string) => {
    const result = await apiCall('/session/status', 'POST', { name });
    if (result.ok) {
      console.log(`Session: ${name}`);
      console.log(`  Claude ID: ${result.claudeSessionId || 'N/A'}`);
      console.log(`  CWD: ${result.cwd}`);
      console.log(`  Created: ${result.created}`);
      
      const stats = result.stats as {
        turns?: number;
        toolCalls?: number;
        tokensIn?: number;
        tokensOut?: number;
        uptime?: number;
        lastActivity?: string;
        isReady?: boolean;
      };
      
      console.log('\nStatistics:');
      console.log(`  Ready: ${stats.isReady ? 'Yes' : 'No'}`);
      console.log(`  Turns: ${stats.turns || 0}`);
      console.log(`  Tool Calls: ${stats.toolCalls || 0}`);
      console.log(`  Tokens In: ${stats.tokensIn || 0}`);
      console.log(`  Tokens Out: ${stats.tokensOut || 0}`);
      console.log(`  Uptime: ${stats.uptime || 0}s`);
      console.log(`  Last Activity: ${stats.lastActivity || 'N/A'}`);
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Get session history
program
  .command('session-history <name>')
  .description('Get conversation history of a persistent session')
  .option('-n, --limit <n>', 'Number of events to show', '20')
  .action(async (name: string, options: { limit: string }) => {
    const result = await apiCall('/session/history', 'POST', { 
      name, 
      limit: parseInt(options.limit) 
    });
    if (result.ok) {
      console.log(`Session '${name}' history (${result.count} events):\n`);
      
      const history = result.history as Array<{
        time: string;
        type: string;
        event: { message?: { content?: Array<{ type: string; text?: string; name?: string }> } };
      }>;
      
      for (const entry of history) {
        const time = new Date(entry.time).toLocaleTimeString();
        const type = entry.type.padEnd(12);
        
        let content = '';
        if (entry.event.message?.content) {
          for (const c of entry.event.message.content) {
            if (c.type === 'text' && c.text) {
              content = c.text.substring(0, 60).replace(/\n/g, ' ');
              if (c.text.length > 60) content += '...';
            } else if (c.type === 'tool_use' && c.name) {
              content = `[Tool: ${c.name}]`;
            }
          }
        }
        
        console.log(`[${time}] ${type} ${content}`);
      }
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Pause a persistent session
program
  .command('session-pause <name>')
  .description('Pause a persistent session (saves state)')
  .action(async (name: string) => {
    const result = await apiCall('/session/pause', 'POST', { name });
    if (result.ok) {
      console.log(`Session '${name}' paused.`);
      console.log(`Resume with: claude-code-skill session-resume ${name}`);
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Resume a paused session
program
  .command('session-resume-paused <name>')
  .description('Resume a paused persistent session')
  .action(async (name: string) => {
    const result = await apiCall('/session/resume', 'POST', { name });
    if (result.ok) {
      console.log(`Session '${name}' resumed.`);
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Fork a session
program
  .command('session-fork <name> <newName>')
  .description('Fork an existing session to create a branch')
  .action(async (name: string, newName: string) => {
    const result = await apiCall('/session/fork', 'POST', { name, newName });
    if (result.ok) {
      console.log(`Session '${name}' forked to '${newName}'`);
      if (result.claudeSessionId) {
        console.log(`New Claude Session ID: ${result.claudeSessionId}`);
      }
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Search sessions
program
  .command('session-search [query]')
  .description('Search sessions by project path, summary, or time')
  .option('-p, --project <path>', 'Filter by project path')
  .option('-s, --since <time>', 'Show sessions since (e.g., "1h", "2d", "2024-01-01")')
  .option('-n, --limit <n>', 'Limit results', '20')
  .action(async (query: string | undefined, options: { project?: string; since?: string; limit: string }) => {
    const result = await apiCall('/session/search', 'POST', {
      query,
      project: options.project,
      since: options.since,
      limit: parseInt(options.limit)
    });

    if (result.ok && Array.isArray(result.sessions)) {
      if (result.sessions.length === 0) {
        console.log('No sessions found.');
      } else {
        console.log(`Found ${result.sessions.length} session(s):\n`);
        for (const s of result.sessions) {
          const sess = s as { name: string; cwd?: string; created?: string; summary?: string };
          console.log(`  ${sess.name}`);
          console.log(`    CWD: ${sess.cwd || 'N/A'}`);
          console.log(`    Created: ${sess.created || 'N/A'}`);
          if (sess.summary) console.log(`    Summary: ${sess.summary}`);
          console.log('');
        }
      }
    } else {
      console.error(`Failed: ${result.error || 'Unknown error'}`);
    }
  });

// Batch read files
program
  .command('batch-read <patterns...>')
  .description('Read multiple files at once by glob patterns')
  .option('-p, --path <dir>', 'Base directory')
  .action(async (patterns: string[], options: { path?: string }) => {
    const result = await apiCall('/batch-read', 'POST', {
      patterns,
      basePath: options.path
    });

    if (result.ok && result.files) {
      const files = result.files as Array<{ path: string; content: string; error?: string }>;
      console.log(`Read ${files.length} file(s):\n`);
      for (const file of files) {
        console.log(`=== ${file.path} ===`);
        if (file.error) {
          console.error(`Error: ${file.error}`);
        } else {
          console.log(file.content);
        }
        console.log('');
      }
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

// Restart a failed session
program
  .command('session-restart <name>')
  .description('Restart a failed or stopped session')
  .action(async (name: string) => {
    const result = await apiCall('/session/restart', 'POST', { name });
    if (result.ok) {
      console.log(`Session '${name}' restarted.`);
    } else {
      console.error(`Failed: ${result.error}`);
    }
  });

program.parse();


path: src/mcp/actions.ts
content: /**
 * MCP Actions - Server-side actions for managing MCP clients
 */

import {
  createClient,
  executeRequest,
  listTools,
  removeClient,
} from "./client";
import { MCPClientLogger } from "./logger";
import {
  DEFAULT_MCP_CONFIG,
  McpClientData,
  McpConfigData,
  McpRequestMessage,
  ServerConfig,
  ServerStatusResponse,
} from "./types";
import fs from "fs/promises";
import path from "path";

const logger = new MCPClientLogger("MCP Actions");

// Default config path - can be overridden
let CONFIG_PATH = path.join(process.cwd(), "mcp_config.json");

const clientsMap = new Map<string, McpClientData>();

// Set custom config path
export function setConfigPath(configPath: string) {
  CONFIG_PATH = configPath;
}

// Get clients status
export async function getClientsStatus(): Promise<
  Record<string, ServerStatusResponse>
> {
  const config = await getMcpConfigFromFile();
  const result: Record<string, ServerStatusResponse> = {};

  for (const clientId of Object.keys(config.mcpServers)) {
    const status = clientsMap.get(clientId);
    const serverConfig = config.mcpServers[clientId];

    if (!serverConfig) {
      result[clientId] = { status: "undefined", errorMsg: null };
      continue;
    }

    if (serverConfig.status === "paused") {
      result[clientId] = { status: "paused", errorMsg: null };
      continue;
    }

    if (!status) {
      result[clientId] = { status: "undefined", errorMsg: null };
      continue;
    }

    if (
      status.client === null &&
      status.tools === null &&
      status.errorMsg === null
    ) {
      result[clientId] = { status: "initializing", errorMsg: null };
      continue;
    }

    if (status.errorMsg) {
      result[clientId] = { status: "error", errorMsg: status.errorMsg };
      continue;
    }

    if (status.client) {
      result[clientId] = { status: "active", errorMsg: null };
      continue;
    }

    result[clientId] = { status: "error", errorMsg: "Client not found" };
  }

  return result;
}

// Get client tools
export async function getClientTools(clientId: string) {
  return clientsMap.get(clientId)?.tools ?? null;
}

// Get available clients count
export async function getAvailableClientsCount() {
  let count = 0;
  clientsMap.forEach((map) => !map.errorMsg && count++);
  return count;
}

// Get all tools from all clients
export async function getAllTools() {
  const result = [];
  for (const [clientId, status] of clientsMap.entries()) {
    result.push({
      clientId,
      tools: status.tools,
    });
  }
  return result;
}

// Initialize single client
async function initializeSingleClient(
  clientId: string,
  serverConfig: ServerConfig,
) {
  if (serverConfig.status === "paused") {
    logger.info(`Skipping initialization for paused client [${clientId}]`);
    return;
  }

  logger.info(`Initializing client [${clientId}]...`);

  clientsMap.set(clientId, {
    client: null,
    tools: null,
    errorMsg: null,
  });

  createClient(clientId, serverConfig)
    .then(async (client) => {
      const tools = await listTools(client);
      logger.info(
        `Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`,
      );
      clientsMap.set(clientId, { client, tools, errorMsg: null });
      logger.success(`Client [${clientId}] initialized successfully`);
    })
    .catch((error) => {
      clientsMap.set(clientId, {
        client: null,
        tools: null,
        errorMsg: error instanceof Error ? error.message : String(error),
      });
      logger.error(`Failed to initialize client [${clientId}]: ${error}`);
    });
}

// Initialize MCP system
export async function initializeMcpSystem() {
  logger.info("MCP Actions starting...");
  try {
    if (clientsMap.size > 0) {
      logger.info("MCP system already initialized, skipping...");
      return;
    }

    const config = await getMcpConfigFromFile();
    for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
      await initializeSingleClient(clientId, serverConfig);
    }
    return config;
  } catch (error) {
    logger.error(`Failed to initialize MCP system: ${error}`);
    throw error;
  }
}

// Add MCP server
export async function addMcpServer(clientId: string, config: ServerConfig) {
  try {
    const currentConfig = await getMcpConfigFromFile();
    const isNewServer = !(clientId in currentConfig.mcpServers);

    if (isNewServer && !config.status) {
      config.status = "active";
    }

    const newConfig = {
      ...currentConfig,
      mcpServers: {
        ...currentConfig.mcpServers,
        [clientId]: config,
      },
    };
    await updateMcpConfig(newConfig);

    if (isNewServer || config.status === "active") {
      await initializeSingleClient(clientId, config);
    }

    return newConfig;
  } catch (error) {
    logger.error(`Failed to add server [${clientId}]: ${error}`);
    throw error;
  }
}

// Pause MCP server
export async function pauseMcpServer(clientId: string) {
  try {
    const currentConfig = await getMcpConfigFromFile();
    const serverConfig = currentConfig.mcpServers[clientId];
    if (!serverConfig) {
      throw new Error(`Server ${clientId} not found`);
    }

    const newConfig: McpConfigData = {
      ...currentConfig,
      mcpServers: {
        ...currentConfig.mcpServers,
        [clientId]: {
          ...serverConfig,
          status: "paused",
        },
      },
    };
    await updateMcpConfig(newConfig);

    const client = clientsMap.get(clientId);
    if (client?.client) {
      await removeClient(client.client);
    }
    clientsMap.delete(clientId);

    return newConfig;
  } catch (error) {
    logger.error(`Failed to pause server [${clientId}]: ${error}`);
    throw error;
  }
}

// Resume MCP server
export async function resumeMcpServer(clientId: string): Promise<void> {
  try {
    const currentConfig = await getMcpConfigFromFile();
    const serverConfig = currentConfig.mcpServers[clientId];
    if (!serverConfig) {
      throw new Error(`Server ${clientId} not found`);
    }

    logger.info(`Trying to initialize client [${clientId}]...`);
    try {
      const client = await createClient(clientId, serverConfig);
      const tools = await listTools(client);
      clientsMap.set(clientId, { client, tools, errorMsg: null });
      logger.success(`Client [${clientId}] initialized successfully`);

      const newConfig: McpConfigData = {
        ...currentConfig,
        mcpServers: {
          ...currentConfig.mcpServers,
          [clientId]: {
            ...serverConfig,
            status: "active" as const,
          },
        },
      };
      await updateMcpConfig(newConfig);
    } catch (error) {
      const currentConfig = await getMcpConfigFromFile();
      const serverConfig = currentConfig.mcpServers[clientId];

      if (serverConfig) {
        serverConfig.status = "error";
        await updateMcpConfig(currentConfig);
      }

      clientsMap.set(clientId, {
        client: null,
        tools: null,
        errorMsg: error instanceof Error ? error.message : String(error),
      });
      logger.error(`Failed to initialize client [${clientId}]: ${error}`);
      throw error;
    }
  } catch (error) {
    logger.error(`Failed to resume server [${clientId}]: ${error}`);
    throw error;
  }
}

// Remove MCP server
export async function removeMcpServer(clientId: string) {
  try {
    const currentConfig = await getMcpConfigFromFile();
    const { [clientId]: _, ...rest } = currentConfig.mcpServers;
    const newConfig = {
      ...currentConfig,
      mcpServers: rest,
    };
    await updateMcpConfig(newConfig);

    const client = clientsMap.get(clientId);
    if (client?.client) {
      await removeClient(client.client);
    }
    clientsMap.delete(clientId);

    return newConfig;
  } catch (error) {
    logger.error(`Failed to remove server [${clientId}]: ${error}`);
    throw error;
  }
}

// Restart all clients
export async function restartAllClients() {
  logger.info("Restarting all clients...");
  try {
    for (const client of clientsMap.values()) {
      if (client.client) {
        await removeClient(client.client);
      }
    }

    clientsMap.clear();

    const config = await getMcpConfigFromFile();
    for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
      await initializeSingleClient(clientId, serverConfig);
    }
    return config;
  } catch (error) {
    logger.error(`Failed to restart clients: ${error}`);
    throw error;
  }
}

// Execute MCP action
export async function executeMcpAction(
  clientId: string,
  request: McpRequestMessage,
) {
  try {
    const client = clientsMap.get(clientId);
    if (!client?.client) {
      throw new Error(`Client ${clientId} not found`);
    }
    logger.info(`Executing request for [${clientId}]`);
    return await executeRequest(client.client, request);
  } catch (error) {
    logger.error(`Failed to execute request for [${clientId}]: ${error}`);
    throw error;
  }
}

// Get MCP config from file
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
  try {
    const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
    return JSON.parse(configStr);
  } catch (error) {
    logger.error(`Failed to load MCP config, using default config: ${error}`);
    return DEFAULT_MCP_CONFIG;
  }
}

// Update MCP config file
async function updateMcpConfig(config: McpConfigData): Promise<void> {
  try {
    await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
    await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
  } catch (error) {
    throw error;
  }
}


path: src/mcp/client.ts
content: /**
 * MCP Client - Creates and manages MCP client connections
 */

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { MCPClientLogger } from "./logger";
import { ListToolsResponse, McpRequestMessage, ServerConfig } from "./types";
import { z } from "zod";

const logger = new MCPClientLogger();

export async function createClient(
  id: string,
  config: ServerConfig,
): Promise<Client> {
  logger.info(`Creating client for ${id}...`);

  const transport = new StdioClientTransport({
    command: config.command,
    args: config.args,
    env: {
      ...Object.fromEntries(
        Object.entries(process.env)
          .filter(([_, v]) => v !== undefined)
          .map(([k, v]) => [k, v as string]),
      ),
      ...(config.env || {}),
    },
  });

  const client = new Client(
    {
      name: `openclaw-mcp-client-${id}`,
      version: "1.0.0",
    },
    {
      capabilities: {},
    },
  );
  await client.connect(transport);
  return client;
}

export async function removeClient(client: Client) {
  logger.info(`Removing client...`);
  await client.close();
}

export async function listTools(client: Client): Promise<ListToolsResponse> {
  return client.listTools();
}

export async function executeRequest(
  client: Client,
  request: McpRequestMessage,
) {
  return client.request(request, z.any());
}


path: src/mcp/index.ts
content: /**
 * MCP Module - Model Context Protocol integration
 */

export * from "./types";
export * from "./logger";
export * from "./client";
export * from "./actions";
export * from "./utils";


path: src/mcp/logger.ts
content: /**
 * MCP Client Logger with ANSI color support
 */

const colors = {
  reset: "\x1b[0m",
  bright: "\x1b[1m",
  dim: "\x1b[2m",
  green: "\x1b[32m",
  yellow: "\x1b[33m",
  red: "\x1b[31m",
  blue: "\x1b[34m",
};

export class MCPClientLogger {
  private readonly prefix: string;
  private readonly debugMode: boolean;

  constructor(
    prefix: string = "OpenClaw MCP Client",
    debugMode: boolean = false,
  ) {
    this.prefix = prefix;
    this.debugMode = debugMode;
  }

  info(message: any) {
    this.print(colors.blue, message);
  }

  success(message: any) {
    this.print(colors.green, message);
  }

  error(message: any) {
    this.print(colors.red, message);
  }

  warn(message: any) {
    this.print(colors.yellow, message);
  }

  debug(message: any) {
    if (this.debugMode) {
      this.print(colors.dim, message);
    }
  }

  private formatMessage(message: any): string {
    return typeof message === "object"
      ? JSON.stringify(message, null, 2)
      : message;
  }

  private print(color: string, message: any) {
    const formattedMessage = this.formatMessage(message);
    const logMessage = `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`;
    console.log(logMessage);
  }
}


path: src/mcp/types.ts
content: /**
 * MCP (Model Context Protocol) Type Definitions
 * Reference: https://spec.modelcontextprotocol.io/specification/basic/messages/
 */

import { z } from "zod";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

// MCP Request Message
export interface McpRequestMessage {
  jsonrpc?: "2.0";
  id?: string | number;
  method: "tools/call" | string;
  params?: {
    [key: string]: unknown;
  };
}

export const McpRequestMessageSchema: z.ZodType<McpRequestMessage> = z.object({
  jsonrpc: z.literal("2.0").optional(),
  id: z.union([z.string(), z.number()]).optional(),
  method: z.string(),
  params: z.record(z.unknown()).optional(),
});

// MCP Response Message
export interface McpResponseMessage {
  jsonrpc?: "2.0";
  id?: string | number;
  result?: {
    [key: string]: unknown;
  };
  error?: {
    code: number;
    message: string;
    data?: unknown;
  };
}

export const McpResponseMessageSchema: z.ZodType<McpResponseMessage> = z.object(
  {
    jsonrpc: z.literal("2.0").optional(),
    id: z.union([z.string(), z.number()]).optional(),
    result: z.record(z.unknown()).optional(),
    error: z
      .object({
        code: z.number(),
        message: z.string(),
        data: z.unknown().optional(),
      })
      .optional(),
  },
);

// MCP Notifications
export interface McpNotifications {
  jsonrpc?: "2.0";
  method: string;
  params?: {
    [key: string]: unknown;
  };
}

export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
  jsonrpc: z.literal("2.0").optional(),
  method: z.string(),
  params: z.record(z.unknown()).optional(),
});

// List Tools Response
export interface ListToolsResponse {
  tools: {
    name?: string;
    description?: string;
    inputSchema?: object;
    [key: string]: any;
  };
}

// MCP Client Data States
export type McpClientData =
  | McpActiveClient
  | McpErrorClient
  | McpInitializingClient;

interface McpInitializingClient {
  client: null;
  tools: null;
  errorMsg: null;
}

interface McpActiveClient {
  client: Client;
  tools: ListToolsResponse;
  errorMsg: null;
}

interface McpErrorClient {
  client: null;
  tools: null;
  errorMsg: string;
}

// Server Status Types
export type ServerStatus =
  | "undefined"
  | "active"
  | "paused"
  | "error"
  | "initializing";

export interface ServerStatusResponse {
  status: ServerStatus;
  errorMsg: string | null;
}

// MCP Server Configuration
export interface ServerConfig {
  command: string;
  args: string[];
  env?: Record<string, string>;
  status?: "active" | "paused" | "error";
}

export interface McpConfigData {
  mcpServers: Record<string, ServerConfig>;
}

export const DEFAULT_MCP_CONFIG: McpConfigData = {
  mcpServers: {},
};

// Arguments Mapping for Preset Servers
export interface ArgsMapping {
  type: "spread" | "single" | "env";
  position?: number;
  key?: string;
}

// Preset Server Configuration
export interface PresetServer {
  id: string;
  name: string;
  description: string;
  repo: string;
  tags: string[];
  command: string;
  baseArgs: string[];
  configurable: boolean;
  configSchema?: {
    properties: Record<
      string,
      {
        type: string;
        description?: string;
        required?: boolean;
        minItems?: number;
      }
    >;
  };
  argsMapping?: Record<string, ArgsMapping>;
}


path: src/mcp/utils.ts
content: /**
 * MCP Utility Functions
 */

/**
 * Check if content contains MCP JSON format
 */
export function isMcpJson(content: string) {
  return content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/);
}

/**
 * Extract MCP JSON from content
 */
export function extractMcpJson(content: string) {
  const match = content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/);
  if (match && match.length === 3) {
    return { clientId: match[1], mcp: JSON.parse(match[2]) };
  }
  return null;
}


path: src/store/index.ts
content: /**
 * Store Module - State management and persistence
 */

export * from "./persist";
export * from "./indexeddb-storage";
export * from "./sync";


path: src/store/indexeddb-storage.ts
content: /**
 * IndexedDB Storage - Persistent storage with localStorage fallback
 */

import { StateStorage } from "zustand/middleware";
import { get, set, del, clear } from "idb-keyval";

/**
 * Safe localStorage wrapper that handles SSR
 */
function safeLocalStorage(): Storage {
  if (typeof window !== "undefined" && window.localStorage) {
    return window.localStorage;
  }
  // Return a mock storage for SSR
  return {
    length: 0,
    clear: () => {},
    getItem: () => null,
    key: () => null,
    removeItem: () => {},
    setItem: () => {},
  };
}

const localStorage = safeLocalStorage();

/**
 * IndexedDB Storage implementation with localStorage fallback
 */
class IndexedDBStorage implements StateStorage {
  public async getItem(name: string): Promise<string | null> {
    try {
      const value = (await get(name)) || localStorage.getItem(name);
      return value;
    } catch (error) {
      return localStorage.getItem(name);
    }
  }

  public async setItem(name: string, value: string): Promise<void> {
    try {
      const _value = JSON.parse(value);
      if (!_value?.state?._hasHydrated) {
        console.warn("skip setItem", name);
        return;
      }
      await set(name, value);
    } catch (error) {
      localStorage.setItem(name, value);
    }
  }

  public async removeItem(name: string): Promise<void> {
    try {
      await del(name);
    } catch (error) {
      localStorage.removeItem(name);
    }
  }

  public async clear(): Promise<void> {
    try {
      await clear();
    } catch (error) {
      localStorage.clear();
    }
  }
}

export const indexedDBStorage = new IndexedDBStorage();


path: src/store/persist.ts
content: /**
 * Persistent Store Factory - Creates Zustand stores with persistence
 */

import { create } from "zustand";
import { combine, persist, createJSONStorage, StateStorage } from "zustand/middleware";

type SecondParam<T> = T extends (
  _f: infer _F,
  _s: infer S,
  ...args: infer _U
) => any
  ? S
  : never;

type MakeUpdater<T> = {
  lastUpdateTime: number;
  _hasHydrated: boolean;

  markUpdate: () => void;
  update: (updater: (state: T) => void) => void;
  setHasHydrated: (state: boolean) => void;
};

type SetStoreState<T> = (
  partial: T | Partial<T> | ((state: T) => T | Partial<T>),
  replace?: boolean | undefined,
) => void;

/**
 * Deep clone utility
 */
function deepClone<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj));
}

/**
 * Create a persistent store with automatic hydration tracking
 */
export function createPersistStore<T extends object, M>(
  state: T,
  methods: (
    set: SetStoreState<T & MakeUpdater<T>>,
    get: () => T & MakeUpdater<T>,
  ) => M,
  persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
  storage?: StateStorage,
) {
  if (storage) {
    persistOptions.storage = createJSONStorage(() => storage);
  }

  const oldOnRehydrateStorage = persistOptions?.onRehydrateStorage;
  persistOptions.onRehydrateStorage = (state) => {
    oldOnRehydrateStorage?.(state);
    return () => state.setHasHydrated(true);
  };

  return create(
    persist(
      combine(
        {
          ...state,
          lastUpdateTime: 0,
          _hasHydrated: false,
        },
        (set, get) => {
          return {
            ...methods(set, get as any),

            markUpdate() {
              set({ lastUpdateTime: Date.now() } as Partial<
                T & M & MakeUpdater<T>
              >);
            },
            update(updater: (state: T) => void) {
              const state = deepClone(get());
              updater(state);
              set({
                ...state,
                lastUpdateTime: Date.now(),
              });
            },
            setHasHydrated: (state: boolean) => {
              set({ _hasHydrated: state } as Partial<T & M & MakeUpdater<T>>);
            },
          } as M & MakeUpdater<T>;
        },
      ),
      persistOptions as any,
    ),
  );
}


path: src/store/sync.ts
content: /**
 * State Sync Utilities - Merge local and remote states
 */

/**
 * Deep merge utility
 */
export function merge<T extends object>(target: T, source: Partial<T>): T {
  for (const key of Object.keys(source) as (keyof T)[]) {
    const sourceValue = source[key];
    const targetValue = target[key];

    if (
      sourceValue !== undefined &&
      typeof sourceValue === "object" &&
      sourceValue !== null &&
      !Array.isArray(sourceValue)
    ) {
      if (typeof targetValue !== "object" || targetValue === null) {
        (target as any)[key] = {};
      }
      merge(target[key] as object, sourceValue as object);
    } else if (sourceValue !== undefined) {
      (target as any)[key] = sourceValue;
    }
  }
  return target;
}

/**
 * Get non-function fields from an object
 */
export function getNonFunctionFields<T extends object>(obj: T) {
  const ret: any = {};

  Object.entries(obj).map(([k, v]) => {
    if (typeof v !== "function") {
      ret[k] = v;
    }
  });

  return ret;
}

/**
 * Merge state with lastUpdateTime - older state will be overridden
 */
export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
  localState: T,
  remoteState: T,
): T {
  const localUpdateTime = localState.lastUpdateTime ?? 0;
  const remoteUpdateTime = remoteState.lastUpdateTime ?? 1;

  if (localUpdateTime < remoteUpdateTime) {
    merge(remoteState, localState);
    return { ...remoteState };
  } else {
    merge(localState, remoteState);
    return { ...localState };
  }
}

/**
 * Session-like data structure for chat history
 */
export interface Session {
  id: string;
  messages: Array<{ id: string; date: string | Date; [key: string]: any }>;
  lastUpdate: string | Date;
  [key: string]: any;
}

/**
 * Merge sessions - combines messages from local and remote
 */
export function mergeSessions(
  localSessions: Session[],
  remoteSessions: Session[],
): Session[] {
  const localSessionMap: Record<string, Session> = {};
  localSessions.forEach((s) => (localSessionMap[s.id] = s));

  remoteSessions.forEach((remoteSession) => {
    // Skip empty chats
    if (remoteSession.messages.length === 0) return;

    const localSession = localSessionMap[remoteSession.id];
    if (!localSession) {
      // Remote session is new, add it
      localSessions.push(remoteSession);
    } else {
      // Both have same session id, merge messages
      const localMessageIds = new Set(localSession.messages.map((v) => v.id));
      remoteSession.messages.forEach((m) => {
        if (!localMessageIds.has(m.id)) {
          localSession.messages.push(m);
        }
      });

      // Sort messages by date ascending
      localSession.messages.sort(
        (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
      );
    }
  });

  // Sort sessions by lastUpdate descending
  localSessions.sort(
    (a, b) =>
      new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
  );

  return localSessions;
}

/**
 * Merge key-value stores - remote values first, then local overrides
 */
export function mergeKeyValueStore<T extends Record<string, any>>(
  localStore: T,
  remoteStore: T,
): T {
  return {
    ...remoteStore,
    ...localStore,
  };
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)