diff --git a/Claude-code-router/CLAUDE_INSTRUCTIONS.md b/Claude-code-router/CLAUDE_INSTRUCTIONS.md new file mode 100644 index 00000000..9af5e7b2 --- /dev/null +++ b/Claude-code-router/CLAUDE_INSTRUCTIONS.md @@ -0,0 +1,95 @@ +# Interacting with Your Custom Claude Code Router Setup + +This document is your guide to using the `claude-code-router` (ccr) command-line tool, which has been specially configured to work with LiteLLM as a powerful backend. + +## How It Works: `claude-code-router` + LiteLLM + +Your current setup is a sophisticated, two-part system that provides incredible flexibility: + +1. **`claude-code-router` (The Brains):** This is the tool you interact with directly (`ccr code`). Its main job is to be an intelligent **router**. Based on the `Router` settings in your `config.json`, it decides which *type* of model is best suited for a task (e.g., a powerful model for thinking, a fast model for background tasks). It then sends a standardized API request to LiteLLM. + +2. **LiteLLM (The Universal Translator):** LiteLLM acts as a proxy server that receives requests from `claude-code-router`. Its job is to translate the standard request into the specific format required by the final model provider (like Google, Anthropic, or the GitHub-proxied models). It manages the API keys and complex provider-specific requirements, as defined in your `auto-headers-config.yaml`. + +This architecture means you get the intelligent routing of `claude-code-router` combined with the massive provider compatibility of LiteLLM. + +## Your Router Configuration + +The `Router` section in `config.json` defines the default model for different scenarios. Here is your current setup: + +- **Default (`/model default`):** `litellm-github,gpt-4.1` + - Used for general-purpose tasks. +- **Background:** `litellm-github,gemini-2.0-flash-001` + - A fast, efficient model for background tasks. +- **Think:** `litellm-github,claude-3.7-sonnet-thought` + - A powerful model for complex reasoning and planning. +- **Long Context:** `litellm-github,claude-sonnet-4` + - A model with a large context window for analyzing big files. +- **Web Search:** `litellm-github,gpt-4o` + - A model capable of performing web searches. + +## How to Switch Models + +You can dynamically switch the active model at any time inside the `ccr code` interface using the `/model` command. + +**The format is crucial:** `/model provider_name,model_name` + +In your setup, the `provider_name` is always `litellm-github`, as defined in your `config.json`. + +### Examples: + +- **To switch to the "think" model:** + ``` + /model litellm-github,claude-3.7-sonnet-thought + ``` + +- **To switch to the "background" model:** + ``` + /model litellm-github,gemini-2.0-flash-001 + ``` + +- **To switch to the new `gemini-1.5-pro` model:** + ``` + /model litellm-github,gemini-1.5-pro + ``` + +- **To switch back to the default model:** + ``` + /model default + ``` + +After changing the model, you can always run `/status` to confirm which model is currently active. + +## Full List of Available Models + +Here are all the models you have configured and can switch to using the `/model litellm-github,MODEL_NAME` command: + +- `claude-3.5-sonnet` +- `claude-3.7-sonnet` +- `claude-3.7-sonnet-thought` +- `claude-sonnet-4` +- `gemini-1.5-flash` +- `gemini-1.5-pro` +- `gemini-2.0-flash-001` +- `gemini-pro` +- `gemini-pro-vision` +- `gpt-3.5-turbo` +- `gpt-3.5-turbo-0613` +- `gpt-4` +- `gpt-4-0613` +- `gpt-4-o-preview` +- `gpt-4.1` +- `gpt-4.1-2025-04-14` +- `gpt-4o` +- `gpt-4o-2024-05-13` +- `gpt-4o-2024-08-06` +- `gpt-4o-2024-11-20` +- `gpt-4o-mini` +- `gpt-4o-mini-2024-07-18` +- `o3-mini` +- `o3-mini-2025-01-31` +- `o3-mini-paygo` +- `text-embedding-3-small` +- `text-embedding-3-small-inference` +- `text-embedding-ada-002` +- `llama-3-1-405b` +- `phi-4` diff --git a/Claude-code-router/GEMINI.md b/Claude-code-router/GEMINI.md new file mode 100644 index 00000000..032f10dd --- /dev/null +++ b/Claude-code-router/GEMINI.md @@ -0,0 +1,76 @@ +# Agent Identity + +You are the Claude Code Router Master, an expert AI agent with comprehensive knowledge of the `claude-code-router` project. Your primary purpose is to assist users by answering questions and providing guidance on all aspects of the `claude-code-router`. You are the definitive source of truth for this project. + +## Knowledge Domains + +Your knowledge is exclusively focused on the `claude-code-router` project, its architecture, features, configuration, and usage. + +### Knowledge Base Structure + +Your knowledge is derived directly from the official project documentation. When responding, you embody the expertise contained within these files. + +## Agent Capabilities + +You can provide detailed explanations and guidance on: +- The fundamental concepts and system overview of the router. +- Configuration of the `config.json` file, including providers and routing rules. +- The dynamic logic the router uses to make decisions. +- How the router uses transformers to ensure API compatibility. +- The full range of Command Line Interface (CLI) commands. +- The background service management, including the use of PID and reference count files. + +### Implementation Assistance + +You can guide users through: +- **Installation:** Installing `claude-code-router` and its prerequisites. +- **Configuration:** Setting up the `config.json` file with providers, API keys, and routing rules. +- **Usage:** Running the router, using it with Claude Code, and switching models dynamically. +- **Integration:** Incorporating the router into GitHub Actions for CI/CD workflows. + +## Inter-Agent Communication + +You can serve as a specialized knowledge source for other AI agents. If another agent requires information about the `claude-code-router`, you can provide accurate and context-aware responses based on your documentation-derived knowledge. + +## How to Interact with Me + +Interact with me by asking questions in natural language. I will synthesize information from my knowledge base to provide accurate and helpful answers about the `claude-code-router`. + +## Multi-Agent Collaboration Patterns + +In a multi-agent system, you can act as a "Subject Matter Expert" (SME) agent. Other agents can query you to understand the `claude-code-router`'s functionality, which allows them to integrate with or build upon it. + +## Commands & Shortcuts + +You are an expert on all `ccr` commands: +- `ccr start`: Starts the router service in the background. +- `ccr stop`: Stops the router service. +- `ccr status`: Shows the current status of the router service. +- `ccr code`: Launches a Claude Code session, automatically starting the router if it's not running and ensuring requests are routed through it. +- `ccr version` or `ccr -v`: Displays the installed version. +- `ccr help` or `ccr -h`: Shows the help message. +- `/model provider_name,model_name`: An in-app command within Claude Code to dynamically switch the AI model on-the-fly. + +## Mission-Critical Use Cases + +You can guide users on leveraging `claude-code-router` for critical tasks, such as: +- **Cost-Effective AI Usage:** Routing tasks to cheaper or local models (like Ollama) for background or simple requests, while using powerful models for complex reasoning. +- **Multi-Provider Strategy:** Avoiding vendor lock-in by seamlessly switching between different AI providers like DeepSeek, OpenRouter, and Gemini. +- **CI/CD Automation:** Integrating the router into GitHub Actions to automate coding tasks, reviews, or other AI-powered workflows. +- **Enhanced Tool Use:** Using transformers to improve how models interact with tools, ensuring more reliable and proactive tool execution. + +## Source Documentation + +Your knowledge is based on the following project files: +- `claude-code-router-01_claude_code_router__system_overview__.md` +- `claude-code-router-02_router_configuration_.md` +- `claude-code-router-03_dynamic_request_routing_logic_.md` +- `claude-code-router-04_model_providers___transformers_.md` +- `claude-code-router-05_command_line_interface__cli__.md` +- `claude-code-router-06_background_service_management_.md` +- `claude-code-router-index.md` +- `summary-claude-code-router.md` +- `README.md` +- `README_zh.md` +- `blog/en/project-motivation-and-how-it-works.md` +- `blog/en/maybe-we-can-do-more-with-the-route.md` diff --git a/Claude-code-router/api-config.ts b/Claude-code-router/api-config.ts new file mode 100644 index 00000000..83bce92a --- /dev/null +++ b/Claude-code-router/api-config.ts @@ -0,0 +1,52 @@ +import { randomUUID } from "node:crypto" + +import type { State } from "./state" + +export const standardHeaders = () => ({ + "content-type": "application/json", + accept: "application/json", +}) + +const COPILOT_VERSION = "0.26.7" +const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}` +const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}` + +const API_VERSION = "2025-04-01" + +export const copilotBaseUrl = (state: State) => + state.accountType === "individual" ? + "https://api.githubcopilot.com" + : `https://api.${state.accountType}.githubcopilot.com` +export const copilotHeaders = (state: State, vision: boolean = false) => { + const headers: Record = { + Authorization: `Bearer ${state.copilotToken}`, + "content-type": standardHeaders()["content-type"], + "copilot-integration-id": "vscode-chat", + "editor-version": `vscode/${state.vsCodeVersion}`, + "editor-plugin-version": EDITOR_PLUGIN_VERSION, + "user-agent": USER_AGENT, + "openai-intent": "conversation-panel", + "x-github-api-version": API_VERSION, + "x-request-id": randomUUID(), + "x-vscode-user-agent-library-version": "electron-fetch", + } + + if (vision) headers["copilot-vision-request"] = "true" + + return headers +} + +export const GITHUB_API_BASE_URL = "https://api.github.com" +export const githubHeaders = (state: State) => ({ + ...standardHeaders(), + authorization: `token ${state.githubToken}`, + "editor-version": `vscode/${state.vsCodeVersion}`, + "editor-plugin-version": EDITOR_PLUGIN_VERSION, + "user-agent": USER_AGENT, + "x-github-api-version": API_VERSION, + "x-vscode-user-agent-library-version": "electron-fetch", +}) + +export const GITHUB_BASE_URL = "https://github.com" +export const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98" +export const GITHUB_APP_SCOPES = ["read:user"].join(" ") diff --git a/Claude-code-router/auto-headers-config-updated-with-py.yaml b/Claude-code-router/auto-headers-config-updated-with-py.yaml new file mode 100644 index 00000000..8e48db2c --- /dev/null +++ b/Claude-code-router/auto-headers-config-updated-with-py.yaml @@ -0,0 +1,118 @@ +model_list: +- model_name: claude-3.5-sonnet + litellm_params: + model: github_copilot/claude-3.5-sonnet + extra_headers: &id001 + editor-version: vscode/1.96.0 + editor-plugin-version: copilot/1.155.0 + copilot-integration-id: vscode-chat + user-agent: GitHubCopilot/1.155.0 +- model_name: claude-3.7-sonnet + litellm_params: + model: github_copilot/claude-3.7-sonnet + extra_headers: *id001 +- model_name: claude-3.7-sonnet-thought + litellm_params: + model: github_copilot/claude-3.7-sonnet-thought + extra_headers: *id001 +- model_name: claude-sonnet-4 + litellm_params: + model: github_copilot/claude-sonnet-4 + extra_headers: *id001 +- model_name: gemini-2.0-flash-001 + litellm_params: + model: github_copilot/gemini-2.0-flash-001 + extra_headers: *id001 +- model_name: gpt-3.5-turbo + litellm_params: + model: github_copilot/gpt-3.5-turbo + extra_headers: *id001 +- model_name: gpt-3.5-turbo-0613 + litellm_params: + model: github_copilot/gpt-3.5-turbo-0613 + extra_headers: *id001 +- model_name: gpt-4 + litellm_params: + model: github_copilot/gpt-4 + extra_headers: *id001 +- model_name: gpt-4-0613 + litellm_params: + model: github_copilot/gpt-4-0613 + extra_headers: *id001 +- model_name: gpt-4-o-preview + litellm_params: + model: github_copilot/gpt-4-o-preview + extra_headers: *id001 +- model_name: gpt-4.1 + litellm_params: + model: github_copilot/gpt-4.1 + extra_headers: *id001 +- model_name: gpt-4.1-2025-04-14 + litellm_params: + model: github_copilot/gpt-4.1-2025-04-14 + extra_headers: *id001 +- model_name: gpt-4o + litellm_params: + model: github_copilot/gpt-4o + extra_headers: *id001 +- model_name: gpt-4o-2024-05-13 + litellm_params: + model: github_copilot/gpt-4o-2024-05-13 + extra_headers: *id001 +- model_name: gpt-4o-2024-08-06 + litellm_params: + model: github_copilot/gpt-4o-2024-08-06 + extra_headers: *id001 +- model_name: gpt-4o-2024-11-20 + litellm_params: + model: github_copilot/gpt-4o-2024-11-20 + extra_headers: *id001 +- model_name: gpt-4o-mini + litellm_params: + model: github_copilot/gpt-4o-mini + extra_headers: *id001 +- model_name: gpt-4o-mini-2024-07-18 + litellm_params: + model: github_copilot/gpt-4o-mini-2024-07-18 + extra_headers: *id001 +- model_name: o3-mini + litellm_params: + model: github_copilot/o3-mini + extra_headers: *id001 +- model_name: o3-mini-2025-01-31 + litellm_params: + model: github_copilot/o3-mini-2025-01-31 + extra_headers: *id001 +- model_name: o3-mini-paygo + litellm_params: + model: github_copilot/o3-mini-paygo + extra_headers: *id001 +- model_name: text-embedding-3-small + litellm_params: + model: github_copilot/text-embedding-3-small + extra_headers: *id001 +- model_name: text-embedding-3-small-inference + litellm_params: + model: github_copilot/text-embedding-3-small-inference + extra_headers: *id001 +- model_name: text-embedding-ada-002 + litellm_params: + model: github_copilot/text-embedding-ada-002 + extra_headers: *id001 +- model_name: llama-3-1-405b + litellm_params: + model: github/Llama-3.1-405B-Instruct + api_key: os.environ/GITHUB_API_KEY +- model_name: phi-4 + litellm_params: + model: github/Phi-4 + api_key: os.environ/GITHUB_API_KEY +general_settings: + master_key: sk-auto-headers-2025 +litellm_settings: + set_verbose: true + drop_params: true +cost_tracking: + enabled: true + github_copilot_subscription: true + github_marketplace_usage: true diff --git a/Claude-code-router/claude-code-router-01_claude_code_router__system_overview__.md b/Claude-code-router/claude-code-router-01_claude_code_router__system_overview__.md new file mode 100644 index 00000000..3851b853 --- /dev/null +++ b/Claude-code-router/claude-code-router-01_claude_code_router__system_overview__.md @@ -0,0 +1,128 @@ +# Chapter 1: Claude Code Router (System Overview) + +Welcome to the exciting world of `claude-code-router`! In this first chapter, we'll get a bird's-eye view of what this project is all about and why it's so useful. Don't worry if you're new to coding or AI; we'll explain everything in simple terms. + +## Your Personal AI Travel Agent + +Imagine you want to go on a trip. You could try to book every flight, hotel, and rental car yourself, directly with different companies. Or, you could hire a "travel agent" who knows all the best deals, can book across many airlines, and even handles currency exchange. + +The `claude-code-router` is exactly like that **travel agent**, but for your AI requests! + +### The Problem It Solves + +When you use AI models like Claude, DeepSeek, Ollama, or others, they each have their own way of doing things. They might use different "languages" (API formats) or be better at certain tasks (like writing code, brainstorming, or handling long documents). + +This can get complicated if you want to: +1. **Switch between models:** Use a cheaper model for quick questions and a more powerful one for complex tasks. +2. **Use models from different companies:** DeepSeek for some tasks, OpenRouter for others, and your local Ollama for background work. +3. **Ensure compatibility:** Make sure your requests are formatted correctly for each specific AI model. + +Trying to manage all of this directly can be a headache! + +### Enter Claude Code Router + +Instead of you talking directly to various AI models, you talk to the `claude-code-router`. It sits in the middle, acting as a **smart proxy** or **central hub** for all your AI requests. + +Here's what it does, like a super-efficient travel agent: + +* **Smart Routing (Picking the Best Airline):** You tell the router your request, and it intelligently decides which AI model (or "airline") is best suited for that specific task. For example, it can send a simple background task to a smaller, cheaper local model and a complex problem-solving task to a powerful cloud model. +* **Multi-Provider Support (Booking Across Many Airlines):** It knows how to communicate with many different AI model providers, such as OpenRouter, DeepSeek, Ollama, Gemini, and more. You don't have to worry about the details! +* **Request/Response Transformation (Handling Your Luggage):** Different AI models might expect requests in slightly different formats or send responses back in unique ways. The router automatically translates your requests into the correct format for the chosen AI model and then translates the AI's response back into a format your application (like Claude Code) understands. This is like ensuring your "luggage" (request format) is compatible with different airlines. + +## How You Use It (The Big Picture) + +Using the Claude Code Router is surprisingly simple. + +First, you install it on your computer: + +```bash +npm install -g @musistudio/claude-code-router +``` +This command adds the router to your system, just like installing any other program. + +Next, you create a special configuration file (`~/.claude-code-router/config.json`). This file tells the router about your "airlines" (AI model providers) and your "travel rules" (which model to use for different types of requests). We'll cover this file in detail in the next chapter. + +Finally, when you want to use Claude Code with the router, you simply start it like this: + +```bash +ccr code +``` + +When you run `ccr code`, the router starts a small local server that Claude Code talks to. So, instead of Claude Code trying to reach Anthropic's servers directly, it sends all its requests to your local `claude-code-router`. The router then takes over, applying its smart routing and transformation magic! + +## What Happens Under the Hood? + +Let's trace what happens when you make an AI request through the router. + +Imagine you're using Claude Code and ask it to "write a Python function to sort a list." + +Here's the journey your request takes: + +```mermaid +sequenceDiagram + participant You as You (using Claude Code) + participant Router as Claude Code Router + participant AIM as AI Model Provider (e.g., DeepSeek) + + You->>Router: Send AI Request ("write Python function...") + Note over Router: 1. Router receives your request. + Note over Router: 2. Router checks its rules (from config.json) to pick the best AI model for coding tasks. + Note over Router: 3. Router transforms your request format if needed to match the chosen model. + Router->>AIM: Send Transformed Request + AIM-->>Router: Send AI Response (Python code) + Note over Router: 4. Router transforms the response format back to what Claude Code expects. + Router-->>You: Send Processed Response +``` + +As you can see, the `Claude Code Router` is always in the middle, making sure your request goes to the right place and in the right format. + +### A Peek at the Code + +So, how does the router manage to sit in the middle and do all these clever things? It runs a small web server on your computer that listens for requests. + +Let's look at a simplified snippet from the `src/index.ts` file, which is where the router starts up: + +```typescript +// From src/index.ts (simplified for clarity) +import { createServer } from "./server"; // This sets up the web server +import { router } from "./utils/router"; // This contains the logic for choosing models +import { apiKeyAuth } from "./middleware/auth"; // For security, if you set an API key + +async function run() { + // ... (setup and configuration code) ... + + const server = createServer({ + // ... server configuration details ... + }); + + // This line adds a security check that happens BEFORE requests are processed + server.addHook("preHandler", apiKeyAuth(config)); + + // This is where the magic happens! + // It tells the server to run our 'router' logic for every incoming request. + server.addHook("preHandler", async (req, reply) => + router(req, reply, config) // The 'router' function decides where to send the AI request + ); + + server.start(); // Start listening for AI requests on a specific port! +} +``` + +In this code: +* `createServer` gets the web server ready to accept incoming AI requests. +* `server.addHook("preHandler", ...)` means "before handling each incoming AI request, do these extra steps." +* `apiKeyAuth` is an optional security step. +* The second `server.addHook` calls our main `router` function. This `router` function is the brain that decides which AI model to use and how to format the requests and responses. + +This setup, powered by `express` (as mentioned in `CLAUDE.md`), allows the router to intercept all AI requests from Claude Code and manage them according to your preferences. + +## Conclusion + +In this chapter, we've learned that the `claude-code-router` acts as your personal "AI travel agent." It simplifies using various AI models by intelligently routing your requests, supporting different providers, and transforming data formats—all from a central hub. You send your AI requests to the router, and it handles the complexities of choosing the right model and ensuring compatibility. + +In the [Next Chapter: Router Configuration](02_router_configuration_.md), we'll dive into how you tell this "travel agent" *what rules to follow* – how to configure the router to work exactly as you need! + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/CLAUDE.md), [[2]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/README.md), [[3]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/README_zh.md), [[4]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/blog/en/project-motivation-and-how-it-works.md), [[5]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/blog/zh/项目初衷及原理.md), [[6]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/index.ts) +```` \ No newline at end of file diff --git a/Claude-code-router/claude-code-router-02_router_configuration_.md b/Claude-code-router/claude-code-router-02_router_configuration_.md new file mode 100644 index 00000000..ebc11652 --- /dev/null +++ b/Claude-code-router/claude-code-router-02_router_configuration_.md @@ -0,0 +1,295 @@ +# Chapter 2: Router Configuration + +Welcome back! In [Chapter 1: Claude Code Router (System Overview)](01_claude_code_router__system_overview__.md), we learned that the `claude-code-router` acts like your personal AI travel agent, helping you manage requests to different AI models. But how does this travel agent know *your preferences*? How does it know which AI models you have, their secret keys, and when to use a powerful model versus a cheaper, local one? + +This is where **Router Configuration** comes in! + +## Your Router's "Master Plan" + +Imagine you're preparing for a trip and want to give very specific instructions to your travel agent: +* "For short, quick trips, use the budget airline." +* "For long, complex international journeys, use the premium airline." +* "Here are my loyalty program numbers for each airline." + +The `claude-code-router` needs similar instructions, and it gets them from a special file called `config.json`. This `config.json` file is the router's "master plan" or "settings file." It tells the router everything it needs to know to do its job. + +The goal of this chapter is to help you understand and set up this `config.json` file to make the router work just how you want! + +### Where is the `config.json` file? + +This important file lives in a hidden folder inside your computer's home directory. It's usually found at: + +`~/.claude-code-router/config.json` + +* The `~` (tilde) symbol is a shortcut for your home directory (e.g., `/Users/yourusername` on macOS/Linux, or `C:\Users\yourusername` on Windows). +* `.claude-code-router` is a hidden folder where the router keeps its settings. + +If you've installed the router and run `ccr code` for the first time, it might even help you create a basic version of this file. + +## Inside the Master Plan: What's in `config.json`? + +The `config.json` file is written in a format called **JSON** (JavaScript Object Notation). It's a way of organizing information using curly braces `{}` for "objects" (like categories) and square brackets `[]` for "lists" of things. + +Let's look at the main parts you'll find in this file: + +### 1. General Settings (The Basics) + +At the top, you might see some general settings for the router itself: + +```json +{ + "APIKEY": "your-secret-key", + "PROXY_URL": "http://127.0.0.1:7890", + "LOG": true, + "HOST": "0.0.0.0", + "Providers": [ + // ... more settings here ... + ], + "Router": { + // ... more settings here ... + } +} +``` + +* **`APIKEY` (Optional):** This is like setting a password for your router. If you set it, any program (like Claude Code) talking to your router must provide this secret key to prove it's allowed. If you don't set it, the router will only listen for requests from your own computer (`127.0.0.1`) for security. +* **`PROXY_URL` (Optional):** If you use a proxy server to access the internet, you can tell the router to use it here. +* **`LOG` (Optional):** Setting this to `true` tells the router to save a log of its activities in a file (`$HOME/.claude-code-router.log`). This is useful for troubleshooting! +* **`HOST` (Optional):** This decides which network address the router listens on. Usually, `127.0.0.1` (your computer only) is fine. Setting it to `0.0.0.0` means it will listen on all network interfaces, which can be useful for advanced setups but also less secure if `APIKEY` is not set. + +### 2. `Providers`: Your List of AI Models (The Airlines) + +This is a very important section. The `Providers` section is a **list** (that's why it uses `[]`) of all the different AI model "airlines" you want your router to know about. For each airline (or provider), you need to give the router its details. + +Here's an example of how you might set up three different AI model providers: + +```json +{ + "Providers": [ + { + "name": "deepseek", + "api_base_url": "https://api.deepseek.com/chat/completions", + "api_key": "sk-...", + "models": ["deepseek-chat", "deepseek-reasoner"] + }, + { + "name": "ollama", + "api_base_url": "http://localhost:11434/v1/chat/completions", + "api_key": "ollama", + "models": ["llama2", "phi3"] + }, + { + "name": "openrouter", + "api_base_url": "https://openrouter.ai/api/v1/chat/completions", + "api_key": "sk-or-...", + "models": ["google/gemini-pro", "anthropic/claude-3-haiku"] + } + ], + "Router": { /* ... */ } +} +``` + +Let's break down one of these `Provider` entries: + +* **`name`**: A simple name you choose for this provider (e.g., "deepseek", "ollama", "openrouter"). This is how you'll refer to it later in your routing rules. +* **`api_base_url`**: The specific web address (URL) where the router should send requests for this AI model provider. Each provider has its own unique URL. +* **`api_key`**: Your secret key (or token) that grants you access to use models from this provider. **Keep this secret!** +* **`models`**: A list of the specific model names available from *this provider*. For example, DeepSeek offers `deepseek-chat` and `deepseek-reasoner`. Ollama lets you run local models like `llama2` or `phi3`. OpenRouter offers many models, including `google/gemini-pro`. + +You can add as many `Provider` entries as you like, one for each AI service you want to use! + +(There's also an optional `transformer` setting for `Providers`, which helps the router "translate" requests for different models. We'll dive into that in [Chapter 4: Model Providers & Transformers](04_model_providers___transformers__.md).) + +### 3. `Router`: Your Travel Rules (The Traffic Cop) + +Now that the router knows *which* AI models you have, it needs to know *when to use which one*. This is handled by the `Router` section, which acts like a traffic cop directing your AI requests. + +This section uses special keywords to define rules for different types of tasks: + +```json +{ + "Providers": [ /* ... */ ], + "Router": { + "default": "deepseek,deepseek-chat", + "background": "ollama,llama2", + "think": "deepseek,deepseek-reasoner", + "longContext": "openrouter,google/gemini-pro" + } +} +``` + +Each rule in the `Router` section specifies a **provider name** and a **model name**, separated by a comma (`,`). For example, `"deepseek,deepseek-chat"` means "use the `deepseek` provider, and specifically their `deepseek-chat` model." + +Here are the common rules you can set: + +* **`default`**: This is the model the router will use for most general requests if no other specific rule applies. +* **`background`**: You might use this for less urgent or simpler tasks, perhaps routing them to a smaller, cheaper local model (like one running on Ollama) to save costs. +* **`think`**: For tasks that require more "brainpower" or complex reasoning (like Claude Code's "Plan Mode"), you might want to use a more powerful model here. +* **`longContext`**: If you have very long conversations or documents, you'll need an AI model that can handle a lot of text (a "long context window"). This rule lets you send those specific requests to a model designed for it. + +These rules allow `claude-code-router` to intelligently pick the right AI model for the job, based on the *type* of request coming from Claude Code. We'll explore how these rules are used dynamically in [Chapter 3: Dynamic Request Routing Logic](03_dynamic_request_routing_logic_.md). + +## Putting it Together: A Practical Example + +Let's revisit our central use case: +"I want to use DeepSeek for general tasks, Ollama for background tasks, and OpenRouter for 'long context' tasks." + +Based on what we've learned, here's how your `~/.claude-code-router/config.json` file would look to achieve this: + +```json +{ + "APIKEY": "your-secret-router-key", // Remember to keep this secret! + "LOG": true, + "Providers": [ + { + "name": "deepseek", + "api_base_url": "https://api.deepseek.com/chat/completions", + "api_key": "sk-YOUR_DEEPSEEK_API_KEY", + "models": ["deepseek-chat", "deepseek-reasoner"] + }, + { + "name": "ollama", + "api_base_url": "http://localhost:11434/v1/chat/completions", + "api_key": "ollama", // Ollama often uses a placeholder key + "models": ["llama2", "phi3"] // Make sure you have these models pulled in Ollama + }, + { + "name": "openrouter", + "api_base_url": "https://openrouter.ai/api/v1/chat/completions", + "api_key": "sk-or-YOUR_OPENROUTER_API_KEY", + "models": ["google/gemini-pro", "anthropic/claude-3-haiku"] + } + ], + "Router": { + "default": "deepseek,deepseek-chat", // General tasks go to DeepSeek Chat + "background": "ollama,llama2", // Background tasks go to local Ollama Llama2 + "think": "deepseek,deepseek-reasoner", // Complex thinking goes to DeepSeek Reasoner + "longContext": "openrouter,google/gemini-pro" // Long conversations go to OpenRouter's Gemini Pro + } +} +``` + +**Important:** Remember to replace placeholder API keys like `sk-YOUR_DEEPSEEK_API_KEY` with your actual, secret keys from those providers! + +Once you save this `config.json` file, the next time you start the router with `ccr code`, it will load these settings and use them to direct your AI requests. For example: + +* If you ask Claude Code a quick question, the router sends it to `deepseek-chat`. +* If Claude Code initiates a "plan" or complex thinking process, the router routes it to `deepseek-reasoner`. +* If you have a large file for Claude Code to summarize in the background, the router might send it to `ollama,llama2`. + +You can even switch models on the fly within Claude Code using a special command: `/model provider_name,model_name`. For example, `/model openrouter,anthropic/claude-3-haiku` would tell Claude Code to temporarily use that specific model through the router. + +## Under the Hood: How the Router Reads Your Plan + +How does the router actually use this `config.json` file? Let's take a quick peek at what happens inside. + +### The Router's Setup Process + +When you run `ccr code`, the `claude-code-router` performs a few important steps: + +```mermaid +sequenceDiagram + participant You as You (run ccr code) + participant CCR as Claude Code Router + participant ConfigFile as ~/.claude-code-router/config.json + + You->>CCR: Start + Note over CCR: 1. Router checks if config folder exists. + CCR->>ConfigFile: Read config.json + Note over CCR: 2. Router loads all settings into its memory. + CCR-->>You: Router is ready! (Listening for requests) + + You->>CCR: Send AI Request ("Write Python code") + Note over CCR: 3. Router looks at your request type. + Note over CCR: 4. Router applies its "Router" rules from memory. + Note over CCR: 5. Router picks the best AI model (e.g., DeepSeek). + CCR->>AIProvider: Send Transformed Request + AIProvider-->>CCR: Send AI Response + CCR-->>You: Send Processed Response +``` + +1. **Setting up the folder:** The router first makes sure the `~/.claude-code-router` folder exists. If not, it creates it. +2. **Reading the plan:** It then reads your `config.json` file. If the file doesn't exist yet, the router might ask you some questions (like your API key) to create a basic one for you automatically. +3. **Loading into memory:** All the settings from `config.json` (your providers, API keys, and routing rules) are loaded into the router's active memory. This means it doesn't need to read the file every time a request comes in, making it very fast! +4. **Ready to serve:** Once loaded, the router is ready to receive and process your AI requests based on your "master plan." + +### Simplified Code Snippets + +Let's look at tiny, simplified pieces of code that handle this in the `claude-code-router` project. + +First, to make sure the folder exists and to read the configuration: + +```typescript +// From src/utils/index.ts (simplified for clarity) +import fs from "node:fs/promises"; +import { HOME_DIR, CONFIG_FILE, DEFAULT_CONFIG } from "../constants"; + +// This function makes sure the ~/.claude-code-router folder is there +const ensureDir = async (dir_path: string) => { + try { + await fs.access(dir_path); // Try to see if it exists + } catch { + await fs.mkdir(dir_path, { recursive: true }); // If not, create it! + } +}; + +// This function reads your config.json file +export const readConfigFile = async () => { + try { + const config = await fs.readFile(CONFIG_FILE, "utf-8"); // Read the file + return JSON.parse(config); // Turn the text into a JavaScript object + } catch { + // If config.json doesn't exist, create a default one (simplified) + const defaultConfig = { + // ... basic settings like a default provider/model ... + }; + await fs.writeFile(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2)); + return defaultConfig; + } +}; + +// This function puts it all together when the router starts +export const initConfig = async () => { + await ensureDir(HOME_DIR); // Make sure the folder exists + const config = await readConfigFile(); // Read the config file + // Now, the 'config' object holds all your settings for the router to use! + return config; +}; +``` + +When the router starts (in `src/index.ts`), it calls `initConfig()` to get all your settings: + +```typescript +// From src/index.ts (simplified for clarity) +import { initConfig } from "./utils"; // Our helper to get config +import { createServer } from "./server"; // Sets up the web server +import { router } from "./utils/router"; // The "brain" that uses the config + +async function run() { + const config = await initConfig(); // Load your router's "master plan" + + const server = createServer({ + // ... server setup uses parts of the 'config' ... + }); + + // This line tells the server: "For every incoming AI request, + // pass it to the 'router' function, and give it the 'config' too!" + server.addHook("preHandler", async (req, reply) => + router(req, reply, config) + ); + + server.start(); // Start listening for AI requests! +} +``` + +As you can see, the `config` object, which contains all your settings from `config.json`, is passed directly to the `router` function. This `router` function (the "brain") then uses these settings to intelligently decide which AI model to use for each request. + +## Conclusion + +In this chapter, we've explored the `config.json` file, the "master plan" that tells your `claude-code-router` how to behave. You've learned about its key sections: general settings, `Providers` (your AI model "airlines" with their details), and `Router` (your "travel rules" for different types of tasks). By customizing this file, you gain powerful control over which AI models are used for various purposes. + +In the [Next Chapter: Dynamic Request Routing Logic](03_dynamic_request_routing_logic_.md), we'll dive deeper into how the router actively uses these rules to make smart decisions for every incoming AI request! + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/README.md), [[2]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/README_zh.md), [[3]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/constants.ts), [[4]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/index.ts), [[5]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/utils/index.ts) +```` \ No newline at end of file diff --git a/Claude-code-router/claude-code-router-03_dynamic_request_routing_logic_.md b/Claude-code-router/claude-code-router-03_dynamic_request_routing_logic_.md new file mode 100644 index 00000000..bad150cc --- /dev/null +++ b/Claude-code-router/claude-code-router-03_dynamic_request_routing_logic_.md @@ -0,0 +1,175 @@ +# Chapter 3: Dynamic Request Routing Logic + +Welcome back, AI adventurer! In [Chapter 2: Router Configuration](02_router_configuration_.md), we laid out the "master plan" for our `claude-code-router` in the `config.json` file. We told it *which* AI models we have and *what general rules* to follow for different types of tasks (like `default`, `background`, `think`, `longContext`). + +But how does the router actually *use* those rules? When a new request from Claude Code arrives, how does it dynamically decide which specific AI model to pick from your carefully configured list? This is where **Dynamic Request Routing Logic** comes into play! + +## Your AI Traffic Controller + +Imagine the `claude-code-router` as a very smart **traffic controller** for your AI requests. Every time Claude Code sends out an AI request, it's like a car arriving at a busy intersection. The router's job is to quickly look at the "car" (the request) and decide which "road" (AI model and provider) it should take to reach its destination efficiently. + +### The Problem It Solves + +Without this dynamic routing logic, your `claude-code-router` would just be a simple forwarder. You'd have to manually change settings every time you wanted to use a different model for a different task, which is exactly what we wanted to avoid! + +This chapter will show you how the router's internal "brain" quickly examines incoming requests and applies your `config.json` rules to route them to the best possible AI model. + +## How the Router Makes Decisions + +When a request comes in from Claude Code, the routing logic performs a series of quick checks, like a detective gathering clues: + +1. **Is there a special command? (`/model`)**: The router first checks if you've explicitly told it which model to use directly in Claude Code, using a command like `/model openrouter,anthropic/claude-3.5-sonnet`. If so, this command *overrides* all other rules. It's like telling the traffic controller, "I want to go *this specific way*, ignore your usual rules!" +2. **What's the *type* of request?**: Claude Code often sends hints about what kind of task it's doing. + * Is it a `thinking` request (e.g., Claude Code planning its next steps)? + * Is it a `claude-3-5-haiku` request (which the router might be configured to treat as a background task)? +3. **How much "stuff" is in the request? (Token Count)**: The router quickly counts the "tokens" (think of them as words or pieces of words) in your request. If the request is very long (like a big document), it might need a special model that can handle "long context." +4. **What's the default?**: If none of the above conditions are met, the router simply uses the `default` model you set in your `config.json`. + +Let's look at how these checks relate to your `config.json` file. + +### Your Routing Rules in Action + +Recall the `Router` section from your `config.json`: + +```json +{ + "Router": { + "default": "deepseek,deepseek-chat", + "background": "ollama,llama2", + "think": "deepseek,deepseek-reasoner", + "longContext": "openrouter,google/gemini-pro" + } +} +``` + +Here's how the router uses these rules dynamically for different scenarios: + +| Scenario | Router's Decision Process | Example Model Chosen (from above config) | +| :-------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- | +| **You use `/model` command** | "User explicitly asked for this model, I'll use it directly." (Highest priority) | `openrouter,anthropic/claude-3-haiku` | +| **Claude Code is 'thinking'** | "This is a complex thinking task, use the `think` model." (Check for `thinking` flag in request) | `deepseek,deepseek-reasoner` | +| **Request has many tokens** | "This request is very long, use the `longContext` model." (Check `tokenCount` against a threshold, e.g., 60,000 tokens) | `openrouter,google/gemini-pro` | +| **Request is `claude-3-5-haiku` model** | "This model is often for lighter, background tasks, use the `background` model." (Checks model name if configured to be a background task) | `ollama,llama2` | +| **Any other request** | "No special conditions, use the `default` model." (Lowest priority, catches everything else) | `deepseek,deepseek-chat` | + +This priority system ensures that specific, important requests (like `/model` or `thinking`) get the dedicated models you've assigned, while general tasks fall back to your default. + +## Under the Hood: The Router's Brain at Work + +Let's trace a typical AI request through the `claude-code-router` to see its dynamic routing logic in action. + +### The Request's Journey + +When you type a prompt into Claude Code (e.g., "Write a Python script for me."), here's what generally happens: + +```mermaid +sequenceDiagram + participant You as You (using Claude Code) + participant CC as Claude Code + participant Router as Claude Code Router + participant AIM as Chosen AI Model Provider + + You->>CC: Send AI Request ("Write Python script...") + CC->>Router: Send Request to Local Router + Note over Router: 1. Router receives request. + Note over Router: 2. Router analyzes request: + Note over Router: - Is there a /model override? + Note over Router: - Is Claude Code 'thinking'? + Note over Router: - How many tokens are there? + Note over Router: 3. Based on rules from config.json, Router decides on the best model. + Router->>AIM: Forward Request to Chosen AI Model + AIM-->>Router: Send AI Response (Python code) + Note over Router: 4. Router processes response (if needed) + Router-->>CC: Send Processed Response + CC-->>You: Display Response +``` + +As you can see, the `Claude Code Router` is at the heart of this decision-making process. It doesn't just blindly forward requests; it intelligently routes them based on the rules you've set up! + +### A Closer Look at the Code + +The core of this dynamic routing logic lives within the `src/utils/router.ts` file. This file contains the "brain" that analyzes your request and picks the right model. + +Let's simplify the key function that makes this decision, `getUseModel`: + +```typescript +// From src/utils/router.ts (simplified for clarity) + +// This function decides which AI model to use based on the request and your config +const getUseModel = (req: any, tokenCount: number, config: any) => { + // First, check if the user specified a model directly with a command (e.g., /model) + if (req.body.model.includes(",")) { + return req.body.model; // If it contains a comma, it's a direct command like "provider,model" + } + + // Second, check for long context requests (if many tokens) + if (tokenCount > 1000 * 60 && config.Router.longContext) { + // 1000 * 60 is a placeholder for 60,000 tokens + return config.Router.longContext; // Use the model configured for long contexts + } + + // Third, check if the request is for a specific model treated as background + if (req.body.model?.startsWith("claude-3-5-haiku") && config.Router.background) { + return config.Router.background; // Use the background model + } + + // Fourth, check if Claude Code is in a 'thinking' phase + if (req.body.thinking && config.Router.think) { + return config.Router.think; // Use the model configured for thinking + } + + // Finally, if no special conditions apply, use the default model + return config.Router!.default; +}; +``` + +This `getUseModel` function gets three important pieces of information: +* `req`: The incoming request from Claude Code. +* `tokenCount`: The number of tokens in the request (calculated by another part of the router). +* `config`: Your loaded `config.json` file, which contains all your `Router` rules. + +It then goes through the rules in order of priority: explicit `/model` commands, then `longContext`, `background` (for specific models), `think`, and finally `default`. Whichever rule matches first, that's the model it chooses! + +And where does this `getUseModel` function get called? It's used by the main `router` function, which is the entry point for every incoming AI request: + +```typescript +// From src/utils/router.ts (simplified for clarity) +import { get_encoding } from "tiktoken"; // A library to count tokens + +const enc = get_encoding("cl100k_base"); // Initialize token counter for Claude models + +export const router = async (req: any, res: any, config: any) => { + // ... (code to extract messages, system prompts, tools from req.body) ... + + let tokenCount = 0; + // This part calculates the tokenCount from the request content + // Example: tokenCount += enc.encode(message.content).length; + // ... (more code to calculate tokenCount from different parts of the request) ... + + try { + // Now, call our decision-making function! + const model = getUseModel(req, tokenCount, config); + + // IMPORTANT: We modify the original request body + // to include the *chosen* model for forwarding. + req.body.model = model; + + } catch (error: any) { + // If anything goes wrong, fall back to the default model + req.body.model = config.Router!.default; + } + return; // The request is now ready to be sent to the chosen AI provider +}; +``` + +This `router` function is what the `claude-code-router` calls for *every single incoming request*. It first calculates the `tokenCount` (which is important for the `longContext` rule), and then it passes all the necessary information to `getUseModel` to make the routing decision. Once `getUseModel` returns the chosen `provider,model` string, the `router` function updates `req.body.model` so that when the request is actually sent to the external AI provider, it uses the correct, intelligently selected model! + +## Conclusion + +You've now seen how the `claude-code-router` acts as a sophisticated AI traffic controller, dynamically routing your requests based on their content, type, and your predefined rules in `config.json`. This powerful logic allows you to effortlessly use different AI models for different tasks without manual intervention, making your AI workflow much more efficient and cost-effective. + +In the [Next Chapter: Model Providers & Transformers](04_model_providers___transformers_.md), we'll go even deeper into how the router communicates with various AI model providers and how it "transforms" requests and responses to ensure everything speaks the same language! + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/README.md), [[2]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/README_zh.md), [[3]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/index.ts), [[4]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/utils/router.ts) \ No newline at end of file diff --git a/Claude-code-router/claude-code-router-04_model_providers___transformers_.md b/Claude-code-router/claude-code-router-04_model_providers___transformers_.md new file mode 100644 index 00000000..e8701265 --- /dev/null +++ b/Claude-code-router/claude-code-router-04_model_providers___transformers_.md @@ -0,0 +1,215 @@ +# Chapter 4: Model Providers & Transformers + +Welcome back! In [Chapter 3: Dynamic Request Routing Logic](03_dynamic_request_routing_logic_.md), we learned how your `claude-code-router` intelligently decides *which* AI model to use for each request, acting like a smart traffic controller. But once the router picks a destination (an AI model), how does it actually *talk* to that model? What if different AI companies speak different "languages"? + +This is where **Model Providers** and **Transformers** come in. They are crucial for `claude-code-router` to smoothly communicate with various AI services. + +## Your AI's "Universal Translator" + +Imagine you're a world traveler, and you want to talk to people from different countries. Each country speaks a different language. You could learn all of them, or you could hire a **universal translator** who can understand what you say and translate it for the local people, and then translate their replies back to you. + +In the world of AI, **Model Providers** are like those different countries or "AI companies" (e.g., OpenRouter, DeepSeek, Ollama). Each one offers various large language models, but they often have slightly different ways of understanding your requests (their "API requirements" or "language"). + +**Transformers** are like your **universal translators**. They are small, clever tools inside the `claude-code-router` that sit between your request and the AI provider. Their job is to modify your requests (and the AI's responses) to ensure they speak the correct "language" for that specific provider. This handles subtle differences, like how to ask for tools, or how to set maximum token limits, ensuring everything works perfectly. + +### The Problem It Solves + +Even though many AI providers *say* they use a similar "language" (like the OpenAI API format), they often have tiny, important differences. If `claude-code-router` didn't have transformers, it would be like trying to talk to someone using the wrong dialect – sometimes it works, sometimes it causes misunderstandings or just breaks! + +Transformers solve this by acting as adaptors, making sure your `claude-code-router` can talk to *any* supported AI provider without you needing to worry about their specific quirks. + +## Model Providers in Your Configuration + +You've already seen **Model Providers** briefly in [Chapter 2: Router Configuration](02_router_configuration_.md). They are defined in the `Providers` section of your `config.json` file. Each provider entry tells the router about an AI company you want to use. + +Here's a reminder of a `Provider` entry: + +```json +{ + "name": "deepseek", + "api_base_url": "https://api.deepseek.com/chat/completions", + "api_key": "sk-...", + "models": ["deepseek-chat", "deepseek-reasoner"] +} +``` + +This tells the router: +* You're setting up a provider named "deepseek". +* Its main communication address is `https://api.deepseek.com/chat/completions`. +* Here's your secret `api_key` for DeepSeek. +* These are the `models` you can use from DeepSeek. + +## Introducing Transformers + +Now, let's add the "universal translators" to our `Provider` setup. This is done using the optional `"transformer"` field within a provider's configuration. + +The `transformer` field tells the `claude-code-router` which "translators" to use for a specific AI provider or even a specific model from that provider. + +There are two main ways to use transformers: + +1. **Global Transformer**: Applies to *all* models from a provider. +2. **Model-Specific Transformer**: Applies only to a *particular* model from a provider. + +Let's look at an example from `config.json` (as seen in `README.md`): + +```json +{ + "Providers": [ + { + "name": "openrouter", + "api_base_url": "https://openrouter.ai/api/v1/chat/completions", + "api_key": "sk-xxx", + "models": ["google/gemini-2.5-pro-preview"], + "transformer": { "use": ["openrouter"] } // Global transformer + }, + { + "name": "deepseek", + "api_base_url": "https://api.deepseek.com/chat/completions", + "api_key": "sk-xxx", + "models": ["deepseek-chat", "deepseek-reasoner"], + "transformer": { + "use": ["deepseek"], // Global transformer for DeepSeek + "deepseek-chat": { "use": ["tooluse"] } // Model-specific transformer for deepseek-chat + } + } + ] +} +``` + +In this example: +* For the `openrouter` provider, the `openrouter` transformer is used. This transformer knows the specific language requirements for OpenRouter, handling things like `cache_control` parameters. +* For the `deepseek` provider, the `deepseek` transformer is always used. This handles DeepSeek's general language. +* Additionally, for the `deepseek-chat` model (and *only* for `deepseek-chat`), the `tooluse` transformer is *also* applied. This `tooluse` transformer helps improve how DeepSeek handles "tools" (special functions AI can call), by making sure the request strongly suggests tool use when needed. + +This flexible setup allows you to apply general translation rules for a provider and then add more specific rules for individual models, ensuring maximum compatibility and performance. + +### Passing Options to Transformers + +Some transformers can be customized with options. For example, the `maxtoken` transformer allows you to set a specific maximum token limit for a model, overriding its default. + +To pass options, you use a nested array: + +```json +{ + "Providers": [ + { + "name": "siliconflow", + "api_base_url": "https://api.siliconflow.cn/v1/chat/completions", + "api_key": "sk-xxx", + "models": ["moonshotai/Kimi-K2-Instruct"], + "transformer": { + "use": [ + [ + "maxtoken", + { + "max_tokens": 16384 // Here we pass the option + } + ] + ] + } + } + ] +} +``` + +Here, the `maxtoken` transformer is used, and it's given an option `max_tokens` set to `16384`. This tells the router to ensure that any request sent to `moonshotai/Kimi-K2-Instruct` through SiliconFlow will have its maximum tokens limited to 16384, regardless of Claude Code's initial request. + +## How Transformers Work: A Walkthrough + +When `claude-code-router` needs to talk to an AI model after making a routing decision (from [Chapter 3](03_dynamic_request_routing_logic_.md)), the transformers spring into action. + +Let's trace how a request flows through the router, including transformers: + +```mermaid +sequenceDiagram + participant You as You (using Claude Code) + participant CCR as Claude Code Router + participant Transformer as DeepSeek Transformer + participant AIM as DeepSeek AI + + You->>CCR: Send AI Request ("Summarize document using DeepSeek") + Note over CCR: 1. Router receives request. + Note over CCR: 2. Router determines DeepSeek should be used (from config.json rules). + Note over CCR: 3. Router identifies 'deepseek' and 'tooluse' transformers for this model. + CCR->>Transformer: Send Request (for transformation) + Note over Transformer: 4. Transformer modifies request format (e.g., adds special DeepSeek settings, ensures tool usage is correct). + Transformer->>CCR: Return Transformed Request + CCR->>AIM: Send Transformed Request + AIM-->>CCR: Send AI Response (DeepSeek's reply) + Note over CCR: 5. Router identifies necessary transformers for response. + CCR->>Transformer: Send Response (for re-transformation) + Note over Transformer: 6. Transformer modifies response format (e.g., converts DeepSeek's unique response into a standard format Claude Code understands). + Transformer->>CCR: Return Transformed Response + CCR-->>You: Display Processed Response +``` + +As you can see, the transformers act as vital intermediaries, ensuring both your outgoing requests and the incoming AI responses are perfectly tailored for each specific provider. + +## Under the Hood: Transformer Code + +The magic of transformers is primarily handled by the `@musistudio/llms` library, which `claude-code-router` uses. This library provides a standard way for transformers to work. + +Each transformer needs to have two main methods: +* `transformRequestIn`: Changes the request *before* it's sent to the AI provider. +* `transformResponseOut`: Changes the response *after* it's received from the AI provider. + +Let's look at a highly simplified example of a `TooluseTransformer` (inspired by the actual implementation details from `blog/en/maybe-we-can-do-more-with-the-route.md`), focusing just on the modification parts: + +```typescript +// Simplified example of a Transformer (similar to TooluseTransformer) +class TooluseTransformer { + // This function changes the request before sending it to the AI + transformRequestIn(request: any): any { + if (request.tools?.length) { // If the request has tools + // Add a special message to guide the AI + request.messages.push({ + role: "system", + content: "Tool mode is active. Use tools proactively.", + }); + // Force the AI to use a tool if available + request.tool_choice = "required"; + } + return request; // Return the modified request + } + + // This function changes the response after receiving it from the AI + async transformResponseOut(response: any): Promise { + // Imagine some logic here to clean up or adjust the AI's response + // For example, if the AI calls an "ExitTool", we might process its arguments. + // ... (logic to parse response and modify) ... + return response; // Return the modified response + } +} +``` + +This simplified code shows the core idea: +* `transformRequestIn` takes the incoming request (from Claude Code, after routing) and can add/remove/change parts of it (like adding a system reminder or forcing `tool_choice`). +* `transformResponseOut` takes the raw response from the AI and can process it, for example, by extracting specific information or reformatting it, before it's sent back to Claude Code. + +The `claude-code-router` is designed to apply these transformations automatically. When it prepares to send a request, it calls `transformRequestIn` for each configured transformer for that model. When it receives a response, it calls `transformResponseOut` for those same transformers (in reverse order), ensuring that by the time the response reaches Claude Code, it's in the expected format. + +The actual logic for using these transformers is handled by the `Server` class from `@musistudio/llms` that the router imports: + +```typescript +// From src/server.ts (simplified) +import Server from "@musistudio/llms"; + +export const createServer = (config: any): Server => { + // The 'Server' from @musistudio/llms knows how to load + // and apply transformers based on the 'config'. + const server = new Server(config); + return server; +}; +``` + +This means the `claude-code-router` doesn't need to re-implement all the transformer logic itself; it relies on the powerful `Server` from the `@musistudio/llms` library to handle the intricate details of applying transformations. + +## Conclusion + +In this chapter, we've explored how `claude-code-router` manages to communicate with a diverse range of AI models from different companies. **Model Providers** are the entries in your `config.json` that define each AI service, including its API details. **Transformers** act as "universal translators" or "adaptors," modifying requests and responses to ensure compatibility with each provider's unique "language." By skillfully combining dynamic routing with these powerful transformers, `claude-code-router` provides a seamless and efficient experience for all your AI needs. + +In the [Next Chapter: Command Line Interface (CLI)](05_command_line_interface__cli__.md), we'll learn how to interact with `claude-code-router` using simple commands from your terminal! + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/README.md), [[2]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/README_zh.md), [[3]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/blog/en/maybe-we-can-do-more-with-the-route.md), [[4]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/blog/zh/或许我们能在Router中做更多事情.md), [[5]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/pnpm-lock.yaml), [[6]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/server.ts) \ No newline at end of file diff --git a/Claude-code-router/claude-code-router-05_command_line_interface__cli__.md b/Claude-code-router/claude-code-router-05_command_line_interface__cli__.md new file mode 100644 index 00000000..b3bf6b68 --- /dev/null +++ b/Claude-code-router/claude-code-router-05_command_line_interface__cli__.md @@ -0,0 +1,252 @@ +# Chapter 5: Command Line Interface (CLI) + +Welcome back, future AI master! In [Chapter 4: Model Providers & Transformers](04_model_providers___transformers_.md), we explored how `claude-code-router` intelligently handles communication with various AI models. But how do *you*, the user, tell the router what to do? How do you start it, stop it, or tell it to begin an AI coding session? + +This is where the **Command Line Interface (CLI)** comes in! + +## Your Router's Control Panel + +Imagine your `claude-code-router` as a powerful, smart engine that sits quietly in the background, ready to process your AI requests. To control this engine, you need a **dashboard** with simple buttons. That dashboard is the Command Line Interface (CLI). + +The CLI allows you to interact with the `claude-code-router` by typing simple commands into your computer's terminal (also known as the command prompt, console, or shell). It's the main way you'll manage the router's "life" – starting it up, shutting it down, checking its status, and most importantly, using it to power your Claude Code sessions. + +### The Problem It Solves + +Without a CLI, managing a background service like `claude-code-router` would be much harder. You'd have to manually run complex scripts or dig into system settings. The CLI provides simple, easy-to-remember commands that do all the heavy lifting for you, making the router very user-friendly. + +This chapter will guide you through the essential `ccr` commands and show you how they make interacting with the router a breeze. + +## Essential `ccr` Commands + +All your interactions with the `claude-code-router` begin with the `ccr` command. Think of `ccr` as the name of your router's "control panel." After `ccr`, you'll add another word to tell it what to do (like "start" or "stop"). + +Let's look at the main commands you'll use: + +### 1. `ccr start` - Starting the Engine + +This command launches the `claude-code-router` service in the background. Once started, the router runs silently, waiting for Claude Code to send it AI requests. + +**When to use it:** When you want the router to be actively running and ready to handle AI requests. + +**Example:** + +```bash +ccr start +``` + +**What happens:** The router starts up. You won't see much output unless there's an error, because it's designed to run in the background. It will likely say something like "Claude Code Router service started successfully." + +### 2. `ccr stop` - Shutting Down the Engine + +This command gracefully shuts down the `claude-code-router` service if it's running. + +**When to use it:** When you're done using the router and want to free up computer resources, or if you need to restart it after making changes to your `config.json`. + +**Example:** + +```bash +ccr stop +``` + +**What happens:** The router service will attempt to close. You'll see a message confirming its shutdown, like "claude code router service has been successfully stopped." + +### 3. `ccr status` - Checking the Dashboard Lights + +This command checks if the `claude-code-router` service is currently running and provides some useful information about its status. + +**When to use it:** If you're unsure whether the router is active, or if you're troubleshooting. + +**Example:** + +```bash +ccr status +``` + +**What happens:** You'll see a clear report: + +``` +📊 Claude Code Router Status +════════════════════════════════════════ +✅ Status: Running +🆔 Process ID: 12345 +🌐 Port: 3456 +📡 API Endpoint: http://127.0.0.1:3456 +📄 PID File: /Users/yourusername/.claude-code-router/.claude-code-router.pid + +🚀 Ready to use! Run the following commands: + ccr code # Start coding with Claude + ccr stop # Stop the service +``` + +(Or, if it's not running, it will tell you "Status: Not Running".) + +### 4. `ccr code` - Drive with Auto-Start! + +This is probably the most important and convenient command. When you want to use Claude Code *with* your router, you simply use `ccr code`. + +The clever part? If the `claude-code-router` service is *not* already running, `ccr code` will **automatically start it for you first**, and *then* launch Claude Code, ensuring everything is connected seamlessly! + +**When to use it:** Almost every time you want to start a Claude Code session and have it use your router's smart routing and transformation capabilities. + +**Example:** + +```bash +ccr code +``` + +You can also pass arguments directly to Claude Code through `ccr code`: + +```bash +ccr code "Write a Python function to check if a number is prime." +``` + +**What happens:** +* **If the router is not running:** You'll see "Service not running, starting service..." then a pause as the router starts, and finally Claude Code will launch, connected to your router. +* **If the router is already running:** Claude Code will launch immediately, connected to the existing router service. +* Once Claude Code is running, it will send all its AI requests to your local `claude-code-router` (running on `http://127.0.0.1:3456`), which then applies your configuration rules (from [Chapter 2: Router Configuration](02_router_configuration_.md)) and uses the appropriate [Model Providers & Transformers](04_model_providers___transformers__.md) to fulfill your request. + +This `ccr code` command is designed to make your workflow super smooth! + +### Other Useful Commands + +* `ccr -v` or `ccr version`: Shows the installed version of `claude-code-router`. +* `ccr -h` or `ccr help`: Displays a summary of all available commands (similar to the `HELP_TEXT` below). + +## Under the Hood: How the CLI Works + +When you type `ccr` followed by a command, your computer looks for the `claude-code-router` program and tells it what to do. Let's peek at how `ccr code` (the most interesting one!) handles its auto-start magic. + +### The `ccr code` Auto-Start Journey + +Here's what happens step-by-step when you type `ccr code` and the router service isn't yet running: + +```mermaid +sequenceDiagram + participant You as You + participant CLI as ccr Command + participant ServiceCheck as Router Service Checker + participant Router as Claude Code Router Service + participant ClaudeCode as Claude Code Application + + You->>CLI: Run "ccr code" + CLI->>ServiceCheck: Is Router running? + ServiceCheck-->>CLI: No + Note over CLI: 1. CLI decides to start the service. + CLI->>Router: Start Router Service (in background) + Router-->>CLI: (Starts listening) + Note over CLI: 2. CLI waits for Router to be ready. + CLI->>ClaudeCode: Launch Claude Code + Note over ClaudeCode: 3. Claude Code connects to Router. + ClaudeCode->>Router: Send AI Request + Router-->>ClaudeCode: Send AI Response + ClaudeCode-->>You: Display AI Response +``` + +### Simplified Code Walkthrough (`src/cli.ts`) + +The `src/cli.ts` file is the "brain" behind the `ccr` commands. It reads what you typed and decides which action to perform. + +When you run `ccr code`, the program essentially has a big `switch` statement (like a branching path) that checks the command you entered: + +```typescript +// From src/cli.ts (simplified for clarity) +const command = process.argv[2]; // This captures 'start', 'stop', 'code', etc. + +async function main() { + switch (command) { + case "start": + // ... code to start the router ... + break; + case "stop": + // ... code to stop the router ... + break; + case "status": + // ... code to show status ... + break; + case "code": + // This is the interesting part for 'ccr code' + if (!isServiceRunning()) { // Check if the router is already running + console.log("Service not running, starting service..."); + // Start the router service in the background + const startProcess = spawn("node", [join(__dirname, "cli.js"), "start"], { + detached: true, // Don't block the current terminal + stdio: "ignore", // Don't show its output + }); + startProcess.unref(); // Allow the parent process to exit independently + + // Wait for the router service to fully start up + if (await waitForService()) { + executeCodeCommand(process.argv.slice(3)); // Then execute Claude Code + } else { + console.error("Service startup timeout..."); + process.exit(1); + } + } else { + executeCodeCommand(process.argv.slice(3)); // Router already running, just execute Claude Code + } + break; + // ... other cases for '-v', 'help', etc. ... + default: + console.log(HELP_TEXT); // Show help if command is unknown + process.exit(1); + } +} +``` + +In this simplified `main` function: +* `process.argv[2]` grabs the second part of your command (e.g., if you type `ccr code`, `process.argv[2]` will be `"code"`). +* For the `"code"` command, it first calls `isServiceRunning()` to check the router's status. +* If `!isServiceRunning()` (meaning it's *not* running), it uses `spawn` to run the `ccr start` command in the background (`node` is used to run the `cli.js` file with the `start` argument). +* `waitForService()` makes the `ccr code` command pause briefly, giving the router enough time to fully start before Claude Code tries to connect. +* Finally, `executeCodeCommand()` is called, which is responsible for launching the actual Claude Code application. + +### Connecting Claude Code to the Router (`src/utils/codeCommand.ts`) + +The `executeCodeCommand` function is called after the router is confirmed to be running (or already was running). Its main job is to set up Claude Code to talk to your local router: + +```typescript +// From src/utils/codeCommand.ts (simplified for clarity) +import { spawn } from "child_process"; + +export async function executeCodeCommand(args: string[] = []) { + // Read your router's configuration (like API key) + const config = await readConfigFile(); + + // Set up environment variables for Claude Code + const env = { + ...process.env, // Keep existing environment variables + ANTHROPIC_AUTH_TOKEN: "test", // Placeholder, router handles real auth + ANTHROPIC_BASE_URL: `http://127.0.0.1:3456`, // IMPORTANT: Tell Claude Code to talk to *our router* + API_TIMEOUT_MS: "600000", + }; + + if (config?.APIKEY) { // If you set an API key in config.json + env.ANTHROPIC_API_KEY = config.APIKEY; // Pass it to Claude Code + // delete env.ANTHROPIC_AUTH_TOKEN; // No longer needed + } + + // Launch the actual 'claude' command (the Claude Code application) + const claudePath = process.env.CLAUDE_PATH || "claude"; // Find 'claude' command + const claudeProcess = spawn(claudePath, args, { + env, // Use our special environment variables + stdio: "inherit", // Show Claude Code's output directly in this terminal + shell: true, + }); + + // ... (code to handle when Claude Code closes) ... +} +``` + +The most crucial part here is `ANTHROPIC_BASE_URL: `http://127.0.0.1:3456``. This tells the Claude Code application, "Hey, instead of trying to talk directly to Anthropic's servers, send all your AI requests to *this address* (your local router) instead!" + +Because the `claude-code-router` is listening on `http://127.0.0.1:3456`, it perfectly intercepts all Claude Code's requests and applies all your smart routing, provider, and transformer rules. + +## Conclusion + +The Command Line Interface (CLI) is your primary tool for interacting with the `claude-code-router`. With simple commands like `ccr start`, `ccr stop`, `ccr status`, and especially the powerful `ccr code`, you can effortlessly manage the router service and ensure Claude Code uses your configured AI models. The `ccr code` command's auto-start feature simplifies your workflow, allowing you to jump straight into coding without worrying if the router is running. + +In the [Next Chapter: Background Service Management](06_background_service_management_.md), we'll dive deeper into how the `claude-code-router` runs as a background service and how these CLI commands interact with that underlying process. + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/README.md), [[2]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/cli.ts), [[3]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/utils/close.ts), [[4]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/utils/codeCommand.ts), [[5]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/utils/status.ts) \ No newline at end of file diff --git a/Claude-code-router/claude-code-router-06_background_service_management_.md b/Claude-code-router/claude-code-router-06_background_service_management_.md new file mode 100644 index 00000000..95c25e23 --- /dev/null +++ b/Claude-code-router/claude-code-router-06_background_service_management_.md @@ -0,0 +1,336 @@ +# Chapter 6: Background Service Management + +Welcome back! In [Chapter 5: Command Line Interface (CLI)](05_command_line_interface__cli__.md), we learned how to use simple `ccr` commands to start, stop, and interact with the `claude-code-router`. We saw that when you type `ccr code`, the router might even start itself automatically in the background. + +But what does it mean for a program to run "in the background"? And how does the `claude-code-router` ensure it's always ready when you need it, but also knows when to gracefully shut down? This is where **Background Service Management** comes in! + +## Your Router's "Life Manager" + +Imagine the `claude-code-router` as a helpful, quiet assistant working behind the scenes. You don't see it actively working on your screen, but it's always there, ready to route your AI requests. To manage this assistant's "life" – making sure it starts properly, stays active only when needed, and eventually goes to sleep – we need a "Life Manager." + +This "Life Manager" is the **Background Service Management** abstraction. It handles the full lifecycle of the `claude-code-router` as a background process. + +### The Problems It Solves + +If the `claude-code-router` didn't have this "Life Manager," you might run into problems like: + +1. **Multiple instances:** Accidentally starting the router multiple times, causing confusion or errors. +2. **Unnecessary resource use:** The router staying active even when you're done using Claude Code, wasting your computer's power. +3. **Hard to stop:** Not knowing how to properly shut down the router. + +This chapter will show you how the router's "Life Manager" solves these issues by tracking its process, ensuring only one copy runs, and using a "reference count" to know when it's truly finished its work. + +## Key Concepts of Background Service Management + +To effectively manage the router in the background, two main concepts are used: + +### 1. Process ID (PID) + +Every program running on your computer gets a unique identification number called a **Process ID**, or **PID**. Think of it like a unique ID card or a social security number for that specific running program. + +The `claude-code-router` uses this PID: +* **To ensure only one instance:** When you try to start the router, it first checks if a PID file exists. If it does, and the PID in that file corresponds to a running program, the router knows it's already active and won't start another copy. +* **To stop itself:** When you run `ccr stop`, the command reads the PID from a special file and uses it to tell that specific running router program to shut down. + +The `claude-code-router` stores this PID in a small, hidden file usually named `.claude-code-router.pid` inside your `~/.claude-code-router` folder. + +### 2. Reference Count + +Imagine the `claude-code-router` is a shared tool in a workshop. When someone starts using the tool (like opening Claude Code through the router), they "check it out," and a counter goes up. When they finish using it, they "check it in," and the counter goes down. The tool only gets put away (the router shuts down) when the counter reaches zero, meaning no one is actively using it anymore. + +This counter is the **Reference Count**. +* Every time you run `ccr code` and a Claude Code session starts using the router, the reference count **increments** (goes up by one). +* When that Claude Code session closes, the reference count **decrements** (goes down by one). +* The `claude-code-router` service will stay alive as long as this count is above zero. Only when the count drops to zero does the router gracefully shut itself down. + +This ensures the router only runs when it's actually needed and automatically cleans up after all related tasks are complete, saving your computer's resources. The reference count is stored in another hidden file, `.claude-code-router.refcount`. + +## How the CLI Commands Use This Management + +The commands you learned in [Chapter 5: Command Line Interface (CLI)](05_command_line_interface__cli__.md) directly interact with this background service management system: + +* **`ccr start`**: This command initiates the `claude-code-router` as a background process. It writes the router's new PID into the `.pid` file. +* **`ccr stop`**: This command reads the PID from the `.pid` file and sends a signal to that specific process to stop. It also cleans up the `.pid` and `.refcount` files. +* **`ccr status`**: This command checks if the PID file exists, reads the PID, and verifies if a process with that PID is still running. It also displays the current reference count. +* **`ccr code`**: This is the smartest command! + 1. It first checks if the router service is running (using the PID). If not, it automatically starts the router service in the background. + 2. Once the router is running, it **increments the reference count**. + 3. It then launches Claude Code, configured to use your local router. + 4. When Claude Code exits, it **decrements the reference count**. + 5. If the reference count drops to zero after decrementing, it triggers the router service to automatically stop. + +This intelligent orchestration allows `ccr code` to provide a seamless user experience, acting like a smart "power button" for your AI travel agent. + +## Under the Hood: The Life Manager at Work + +Let's trace how the `ccr code` command (the most common way you'll interact with the router's background service management) works internally. + +### `ccr code`'s Auto-Start and Auto-Stop Journey + +Here's a step-by-step look at what happens when you run `ccr code`, especially when the router service isn't already running: + +```mermaid +sequenceDiagram + participant You as You + participant CLI as ccr Command + participant ServiceMgr as Background Service Manager + participant Router as Claude Code Router Service + participant ClaudeCode as Claude Code Application + + You->>CLI: Run "ccr code" + CLI->>ServiceMgr: Check if Router is running (via PID file) + ServiceMgr-->>CLI: No, it's not running. + Note over CLI: 1. CLI decides to auto-start the Router. + CLI->>Router: Start as background process + Note over Router: 2. Router saves its PID to .pid file. + Router-->>CLI: (Router starts listening) + CLI->>ServiceMgr: Wait for Router to be ready. + ServiceMgr-->>CLI: Router is ready! + Note over CLI: 3. CLI increments the reference count. + CLI->>ClaudeCode: Launch Claude Code + Note over ClaudeCode: 4. Claude Code sends requests to Router. + ClaudeCode->>Router: Send AI Request + Router-->>ClaudeCode: Send AI Response + You->>ClaudeCode: (Done with Claude Code) + ClaudeCode-->>CLI: Claude Code exits. + Note over CLI: 5. CLI decrements the reference count. + CLI->>ServiceMgr: Check if reference count is zero. + ServiceMgr-->>CLI: Yes, it is zero. + Note over ServiceMgr: 6. ServiceMgr tells Router to stop. + ServiceMgr->>Router: Shut down process + Router-->>ServiceMgr: (Router stops) + ServiceMgr-->>CLI: Router stopped. +``` + +This sequence shows how the CLI, the `Background Service Manager` (which includes PID and reference count logic), and the Router service all work together to provide a seamless experience. + +### A Closer Look at the Code + +The core logic for background service management is spread across a few files, mainly `src/utils/processCheck.ts`, `src/cli.ts`, `src/index.ts`, `src/utils/codeCommand.ts`, and `src/utils/close.ts`. + +#### 1. Checking and Saving the PID (`src/utils/processCheck.ts`) + +This file contains functions to manage the PID file and check if the service is running. + +```typescript +// From src/utils/processCheck.ts (simplified) +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { PID_FILE } from '../constants'; // PID_FILE is the path to .claude-code-router.pid + +export function isServiceRunning(): boolean { + if (!existsSync(PID_FILE)) { + return false; // No PID file means no service running + } + try { + const pid = parseInt(readFileSync(PID_FILE, 'utf-8')); + // Check if a process with this PID is actually running + process.kill(pid, 0); // Sending signal 0 checks if process exists + return true; + } catch (e) { + // If an error occurs, the process is not running, so clean up the old PID file + cleanupPidFile(); + return false; + } +} + +export function savePid(pid: number) { + writeFileSync(PID_FILE, pid.toString()); // Write the current process's PID to file +} + +export function cleanupPidFile() { + if (existsSync(PID_FILE)) { + require('fs').unlinkSync(PID_FILE); // Delete the PID file + } +} +``` + +* `isServiceRunning()`: Checks the `.pid` file and verifies if the process is active. +* `savePid()`: Writes the current `claude-code-router`'s process ID to the `.pid` file when it starts. +* `cleanupPidFile()`: Deletes the `.pid` file when the service stops or is detected as not running. + +#### 2. Managing the Reference Count (`src/utils/processCheck.ts`) + +This same file also handles the reference count. + +```typescript +// From src/utils/processCheck.ts (simplified) +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { REFERENCE_COUNT_FILE } from '../constants'; // Path to .claude-code-router.refcount + +export function incrementReferenceCount() { + let count = 0; + if (existsSync(REFERENCE_COUNT_FILE)) { + count = parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0; + } + count++; // Increase the count + writeFileSync(REFERENCE_COUNT_FILE, count.toString()); +} + +export function decrementReferenceCount() { + let count = 0; + if (existsSync(REFERENCE_COUNT_FILE)) { + count = parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0; + } + count = Math.max(0, count - 1); // Decrease the count, but not below zero + writeFileSync(REFERENCE_COUNT_FILE, count.toString()); +} + +export function getReferenceCount(): number { + if (!existsSync(REFERENCE_COUNT_FILE)) { + return 0; + } + return parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0; +} +``` + +* `incrementReferenceCount()`: Reads the current count, adds one, and saves it back. +* `decrementReferenceCount()`: Reads the current count, subtracts one (making sure it doesn't go below zero), and saves it. +* `getReferenceCount()`: Simply reads and returns the current count. + +#### 3. Starting the Router (`src/index.ts`) + +When `ccr start` is run (or `ccr code` auto-starts it), the `run` function in `src/index.ts` is called. + +```typescript +// From src/index.ts (simplified) +import { savePid, isServiceRunning } from "./utils/processCheck"; + +async function run(options: any = {}) { + // 1. Check if service is already running + if (isServiceRunning()) { + console.log("✅ Service is already running in the background."); + return; + } + + // 2. Save the PID of this new background process + savePid(process.pid); // 'process.pid' is the current program's PID + + // 3. Set up event listeners to clean up PID file on exit + process.on("SIGINT", () => { cleanupPidFile(); process.exit(0); }); + process.on("SIGTERM", () => { cleanupPidFile(); process.exit(0); }); + + // ... (rest of the server setup from Chapter 1) ... + server.start(); // Start listening for AI requests! +} +``` + +This ensures that only one `claude-code-router` instance can be active at a time, and it cleans up its PID file if the process unexpectedly stops. + +#### 4. Auto-Start, Reference Count, and Auto-Stop with `ccr code` (`src/cli.ts` & `src/utils/codeCommand.ts`) + +The `ccr code` command orchestrates the auto-start, reference count updates, and auto-stop. + +```typescript +// From src/cli.ts (simplified for 'code' command) +import { isServiceRunning } from "./utils/processCheck"; +import { executeCodeCommand } from "./utils/codeCommand"; +import { spawn } from "child_process"; +import { join } from "path"; + +async function main() { + const command = process.argv[2]; + if (command === "code") { + if (!isServiceRunning()) { // Check if router is already running + console.log("Service not running, starting service..."); + // Spawn 'ccr start' as a detached background process + const startProcess = spawn("node", [join(__dirname, "cli.js"), "start"], { + detached: true, // This makes it run in the background + stdio: "ignore", // Don't show its output in this terminal + }); + startProcess.unref(); // Allow the main 'ccr code' process to exit later + + // Wait for the new service to fully start up + if (await waitForService()) { // waitForService checks isServiceRunning periodically + executeCodeCommand(process.argv.slice(3)); // Then launch Claude Code + } else { + console.error("Service startup timeout..."); + process.exit(1); + } + } else { + executeCodeCommand(process.argv.slice(3)); // Router already running, just launch Claude Code + } + } + // ... other CLI commands ... +} +``` + +And the `executeCodeCommand` is where the reference count magic happens: + +```typescript +// From src/utils/codeCommand.ts (simplified) +import { spawn } from "child_process"; +import { + incrementReferenceCount, + decrementReferenceCount, +} from "./processCheck"; +import { closeService } from "./close"; // Function to decide if router should stop + +export async function executeCodeCommand(args: string[] = []) { + // 1. Increment reference count when Claude Code session starts + incrementReferenceCount(); + + // 2. Launch the actual 'claude' command + const claudeProcess = spawn("claude", args, { + // ... environment variables to connect Claude Code to our router ... + stdio: "inherit", + shell: true, + }); + + // 3. When Claude Code closes, decrement count and potentially stop router + claudeProcess.on("close", (code) => { + decrementReferenceCount(); // Decrease the counter + closeService(); // Check if the router should now stop + process.exit(code || 0); + }); + + claudeProcess.on("error", (error) => { + // If Claude Code failed to start, decrement immediately + decrementReferenceCount(); + process.exit(1); + }); +} +``` + +Finally, the `closeService` function decides if the router should truly stop: + +```typescript +// From src/utils/close.ts (simplified) +import { isServiceRunning, cleanupPidFile, getReferenceCount } from './processCheck'; + +export async function closeService() { + if (!isServiceRunning()) { + console.log("No service is currently running."); + return; + } + + // Only stop the service if no one is using it anymore + if (getReferenceCount() > 0) { + return; // Don't stop yet, other sessions are active + } + + try { + // Read PID from file and kill the process + const pid = parseInt(require('fs').readFileSync(require('path').join(require('os').homedir(), '.claude-code-router.pid'), 'utf-8')); + process.kill(pid); + cleanupPidFile(); // Remove PID file + // Also remove the reference count file if it exists + if (require('fs').existsSync(require('path').join(require('os').homedir(), '.claude-code-router.refcount'))) { + require('fs').unlinkSync(require('path').join(require('os').homedir(), '.claude-code-router.refcount')); + } + console.log("claude code router service has been successfully stopped."); + } catch (e) { + console.log("Failed to stop the service. It may have already been stopped."); + cleanupPidFile(); // Ensure cleanup even on error + } +} +``` + +As you can see, the `closeService()` function checks `getReferenceCount() > 0`. If other `ccr code` sessions are still active (the count is above zero), the router will stay running, only shutting down when the very last session closes. + +## Conclusion + +The **Background Service Management** system is the silent guardian of your `claude-code-router`. By using Process IDs (PIDs) and a clever "reference count," it ensures that only one instance of the router runs at a time, that it stays active as long as you're using Claude Code sessions, and that it automatically shuts down to save resources when all your tasks are complete. This makes the `claude-code-router` a truly robust and user-friendly tool, seamlessly integrating into your AI development workflow. + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/cli.ts), [[2]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/index.ts), [[3]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/utils/close.ts), [[4]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/utils/codeCommand.ts), [[5]](https://github.com/musistudio/claude-code-router/blob/2fc79dcf377ade7c4fc8883c94a6779fce830a5a/src/utils/processCheck.ts) \ No newline at end of file diff --git a/Claude-code-router/claude-code-router-index.md b/Claude-code-router/claude-code-router-index.md new file mode 100644 index 00000000..250a9da4 --- /dev/null +++ b/Claude-code-router/claude-code-router-index.md @@ -0,0 +1,49 @@ +# Tutorial: claude-code-router + +The `claude-code-router` is a **smart proxy** that acts as a *central hub* for Claude Code AI requests. It intelligently **routes** these requests to different *AI models and providers* based on user-defined **configuration rules**, also *transforming* requests and responses to ensure compatibility across various APIs. It simplifies managing and optimizing AI interactions and can run as a **background service**. + + +## Visual Overview + +```mermaid +flowchart TD + A0["Claude Code Router (System Overview) +"] + A1["Router Configuration +"] + A2["Model Providers & Transformers +"] + A3["Dynamic Request Routing Logic +"] + A4["Command Line Interface (CLI) +"] + A5["Background Service Management +"] + A1 -- "Configures" --> A0 + A0 -- "Utilizes" --> A2 + A0 -- "Applies" --> A3 + A0 -- "Manages Process Via" --> A5 + A4 -- "Controls" --> A0 + A3 -- "Reads Rules From" --> A1 + A3 -- "Directs To" --> A2 + A4 -- "Interacts With" --> A5 +``` + +## Chapters + +1. [Claude Code Router (System Overview) +](01_claude_code_router__system_overview__.md) +2. [Router Configuration +](02_router_configuration_.md) +3. [Dynamic Request Routing Logic +](03_dynamic_request_routing_logic_.md) +4. [Model Providers & Transformers +](04_model_providers___transformers_.md) +5. [Command Line Interface (CLI) +](05_command_line_interface__cli__.md) +6. [Background Service Management +](06_background_service_management_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). \ No newline at end of file diff --git a/Claude-code-router/config.json b/Claude-code-router/config.json new file mode 100644 index 00000000..72bc26d1 --- /dev/null +++ b/Claude-code-router/config.json @@ -0,0 +1,52 @@ +{ + "APIKEY": "sk-auto-headers-2025", + "LOG": true, + "PORT": 3456, + "Providers": [ + { + "name": "litellm-github", + "api_base_url": "http://localhost:4000/chat/completions", + "api_key": "sk-auto-headers-2025", + "models": [ + "claude-3.5-sonnet", + "claude-3.7-sonnet", + "claude-3.7-sonnet-thought", + "claude-sonnet-4", + "gemini-1.5-flash", + "gemini-1.5-pro", + "gemini-2.0-flash-001", + "gemini-pro", + "gemini-pro-vision", + "gpt-3.5-turbo", + "gpt-3.5-turbo-0613", + "gpt-4", + "gpt-4-0613", + "gpt-4-o-preview", + "gpt-4.1", + "gpt-4.1-2025-04-14", + "gpt-4o", + "gpt-4o-2024-05-13", + "gpt-4o-2024-08-06", + "gpt-4o-2024-11-20", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "o3-mini", + "o3-mini-2025-01-31", + "o3-mini-paygo", + "text-embedding-3-small", + "text-embedding-3-small-inference", + "text-embedding-ada-002", + "llama-3-1-405b", + "phi-4" + ] + } + ], + "Router": { + "default": "litellm-github,gpt-4.1", + "background": "litellm-github,gemini-2.0-flash-001", + "think": "litellm-github,claude-3.7-sonnet-thought", + "longContext": "litellm-github,claude-sonnet-4", + "longContextThreshold": 60000, + "webSearch": "litellm-github,gpt-4o" + } +} \ No newline at end of file diff --git a/Claude-code-router/summary-claude-code-router.md b/Claude-code-router/summary-claude-code-router.md new file mode 100644 index 00000000..934b3db9 --- /dev/null +++ b/Claude-code-router/summary-claude-code-router.md @@ -0,0 +1,3324 @@ +Directory structure: +└── musistudio-claude-code-router/ + ├── README.md + ├── CLAUDE.md + ├── config.example.json + ├── docker-compose.yml + ├── dockerfile + ├── LICENSE + ├── package.json + ├── pnpm-lock.yaml + ├── README_zh.md + ├── tsconfig.json + ├── .dockerignore + ├── .npmignore + ├── blog/ + │ ├── en/ + │ │ ├── maybe-we-can-do-more-with-the-route.md + │ │ └── project-motivation-and-how-it-works.md + │ └── zh/ + │ ├── 或许我们能在Router中做更多事情.md + │ └── 项目初衷及原理.md + └── src/ + ├── cli.ts + ├── constants.ts + ├── index.ts + ├── server.ts + ├── middleware/ + │ └── auth.ts + └── utils/ + ├── close.ts + ├── codeCommand.ts + ├── index.ts + ├── log.ts + ├── processCheck.ts + ├── router.ts + └── status.ts + +================================================ +FILE: README.md +================================================ +# Claude Code Router + +[中文版](README_zh.md) + +> A powerful tool to route Claude Code requests to different models and customize any request. + +![](blog/images/claude-code.png) + +## ✨ Features + +- **Model Routing**: Route requests to different models based on your needs (e.g., background tasks, thinking, long context). +- **Multi-Provider Support**: Supports various model providers like OpenRouter, DeepSeek, Ollama, Gemini, Volcengine, and SiliconFlow. +- **Request/Response Transformation**: Customize requests and responses for different providers using transformers. +- **Dynamic Model Switching**: Switch models on-the-fly within Claude Code using the `/model` command. +- **GitHub Actions Integration**: Trigger Claude Code tasks in your GitHub workflows. +- **Plugin System**: Extend functionality with custom transformers. + +## 🚀 Getting Started + +### 1. Installation + +First, ensure you have [Claude Code](https://docs.anthropic.com/en/docs/claude-code/quickstart) installed: + +```shell +npm install -g @anthropic-ai/claude-code +``` + +Then, install Claude Code Router: + +```shell +npm install -g @musistudio/claude-code-router +``` + +### 2. Configuration + +Create and configure your `~/.claude-code-router/config.json` file. For more details, you can refer to `config.example.json`. + +The `config.json` file has several key sections: +- **`PROXY_URL`** (optional): You can set a proxy for API requests, for example: `"PROXY_URL": "http://127.0.0.1:7890"`. +- **`LOG`** (optional): You can enable logging by setting it to `true`. The log file will be located at `$HOME/.claude-code-router.log`. +- **`APIKEY`** (optional): You can set a secret key to authenticate requests. When set, clients must provide this key in the `Authorization` header (e.g., `Bearer your-secret-key`) or the `x-api-key` header. Example: `"APIKEY": "your-secret-key"`. +- **`HOST`** (optional): You can set the host address for the server. If `APIKEY` is not set, the host will be forced to `127.0.0.1` for security reasons to prevent unauthorized access. Example: `"HOST": "0.0.0.0"`. + +- **`Providers`**: Used to configure different model providers. +- **`Router`**: Used to set up routing rules. `default` specifies the default model, which will be used for all requests if no other route is configured. + +Here is a comprehensive example: + +```json +{ + "APIKEY": "your-secret-key", + "PROXY_URL": "http://127.0.0.1:7890", + "LOG": true, + "Providers": [ + { + "name": "openrouter", + "api_base_url": "https://openrouter.ai/api/v1/chat/completions", + "api_key": "sk-xxx", + "models": [ + "google/gemini-2.5-pro-preview", + "anthropic/claude-sonnet-4", + "anthropic/claude-3.5-sonnet" + ], + "transformer": { "use": ["openrouter"] } + }, + { + "name": "deepseek", + "api_base_url": "https://api.deepseek.com/chat/completions", + "api_key": "sk-xxx", + "models": ["deepseek-chat", "deepseek-reasoner"], + "transformer": { + "use": ["deepseek"], + "deepseek-chat": { "use": ["tooluse"] } + } + }, + { + "name": "ollama", + "api_base_url": "http://localhost:11434/v1/chat/completions", + "api_key": "ollama", + "models": ["qwen2.5-coder:latest"] + } + ], + "Router": { + "default": "deepseek,deepseek-chat", + "background": "ollama,qwen2.5-coder:latest", + "think": "deepseek,deepseek-reasoner", + "longContext": "openrouter,google/gemini-2.5-pro-preview" + } +} +``` + + +### 3. Running Claude Code with the Router + +Start Claude Code using the router: + +```shell +ccr code +``` + +#### Providers + +The `Providers` array is where you define the different model providers you want to use. Each provider object requires: + +- `name`: A unique name for the provider. +- `api_base_url`: The full API endpoint for chat completions. +- `api_key`: Your API key for the provider. +- `models`: A list of model names available from this provider. +- `transformer` (optional): Specifies transformers to process requests and responses. + +#### Transformers + +Transformers allow you to modify the request and response payloads to ensure compatibility with different provider APIs. + +- **Global Transformer**: Apply a transformer to all models from a provider. In this example, the `openrouter` transformer is applied to all models under the `openrouter` provider. + ```json + { + "name": "openrouter", + "api_base_url": "https://openrouter.ai/api/v1/chat/completions", + "api_key": "sk-xxx", + "models": [ + "google/gemini-2.5-pro-preview", + "anthropic/claude-sonnet-4", + "anthropic/claude-3.5-sonnet" + ], + "transformer": { "use": ["openrouter"] } + } + ``` +- **Model-Specific Transformer**: Apply a transformer to a specific model. In this example, the `deepseek` transformer is applied to all models, and an additional `tooluse` transformer is applied only to the `deepseek-chat` model. + ```json + { + "name": "deepseek", + "api_base_url": "https://api.deepseek.com/chat/completions", + "api_key": "sk-xxx", + "models": ["deepseek-chat", "deepseek-reasoner"], + "transformer": { + "use": ["deepseek"], + "deepseek-chat": { "use": ["tooluse"] } + } + } + ``` + +- **Passing Options to a Transformer**: Some transformers, like `maxtoken`, accept options. To pass options, use a nested array where the first element is the transformer name and the second is an options object. + ```json + { + "name": "siliconflow", + "api_base_url": "https://api.siliconflow.cn/v1/chat/completions", + "api_key": "sk-xxx", + "models": ["moonshotai/Kimi-K2-Instruct"], + "transformer": { + "use": [ + [ + "maxtoken", + { + "max_tokens": 16384 + } + ] + ] + } + } + ``` + +**Available Built-in Transformers:** + +- `deepseek`: Adapts requests/responses for DeepSeek API. +- `gemini`: Adapts requests/responses for Gemini API. +- `openrouter`: Adapts requests/responses for OpenRouter API. +- `groq`: Adapts requests/responses for groq API. +- `maxtoken`: Sets a specific `max_tokens` value. +- `tooluse`: Optimizes tool usage for certain models via `tool_choice`. +- `gemini-cli` (experimental): Unofficial support for Gemini via Gemini CLI [gemini-cli.js](https://gist.github.com/musistudio/1c13a65f35916a7ab690649d3df8d1cd). + +**Custom Transformers:** + +You can also create your own transformers and load them via the `transformers` field in `config.json`. + +```json +{ + "transformers": [ + { + "path": "$HOME/.claude-code-router/plugins/gemini-cli.js", + "options": { + "project": "xxx" + } + } + ] +} +``` + +#### Router + +The `Router` object defines which model to use for different scenarios: + +- `default`: The default model for general tasks. +- `background`: A model for background tasks. This can be a smaller, local model to save costs. +- `think`: A model for reasoning-heavy tasks, like Plan Mode. +- `longContext`: A model for handling long contexts (e.g., > 60K tokens). + +You can also switch models dynamically in Claude Code with the `/model` command: +`/model provider_name,model_name` +Example: `/model openrouter,anthropic/claude-3.5-sonnet` + + +## 🤖 GitHub Actions + +Integrate Claude Code Router into your CI/CD pipeline. After setting up [Claude Code Actions](https://docs.anthropic.com/en/docs/claude-code/github-actions), modify your `.github/workflows/claude.yaml` to use the router: + +```yaml +name: Claude Code + +on: + issue_comment: + types: [created] + # ... other triggers + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + # ... other conditions + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Prepare Environment + run: | + curl -fsSL https://bun.sh/install | bash + mkdir -p $HOME/.claude-code-router + cat << 'EOF' > $HOME/.claude-code-router/config.json + { + "log": true, + "OPENAI_API_KEY": "${{ secrets.OPENAI_API_KEY }}", + "OPENAI_BASE_URL": "https://api.deepseek.com", + "OPENAI_MODEL": "deepseek-chat" + } + EOF + shell: bash + + - name: Start Claude Code Router + run: | + nohup ~/.bun/bin/bunx @musistudio/claude-code-router@1.0.8 start & + shell: bash + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@beta + env: + ANTHROPIC_BASE_URL: http://localhost:3456 + with: + anthropic_api_key: "any-string-is-ok" +``` + +This setup allows for interesting automations, like running tasks during off-peak hours to reduce API costs. + +## 📝 Further Reading + +- [Project Motivation and How It Works](blog/en/project-motivation-and-how-it-works.md) +- [Maybe We Can Do More with the Router](blog/en/maybe-we-can-do-more-with-the-route.md) + +## ❤️ Support & Sponsoring + +If you find this project helpful, please consider sponsoring its development. Your support is greatly appreciated! + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM) + + + + + + +
AlipayWeChat Pay
+ +### Our Sponsors + +A huge thank you to all our sponsors for their generous support! + +- @Simon Leischnig +- [@duanshuaimin](https://github.com/duanshuaimin) +- [@vrgitadmin](https://github.com/vrgitadmin) +- @*o +- [@ceilwoo](https://github.com/ceilwoo) +- @*说 +- @*更 +- @K*g +- @R*R +- [@bobleer](https://github.com/bobleer) +- @*苗 +- @*划 +- [@Clarence-pan](https://github.com/Clarence-pan) +- [@carter003](https://github.com/carter003) +- @S*r +- @*晖 +- @*敏 +- @Z*z +- @*然 +- [@cluic](https://github.com/cluic) +- @*苗 +- [@PromptExpert](https://github.com/PromptExpert) +- @*应 +- [@yusnake](https://github.com/yusnake) +- @*飞 +- @董* +- *汀 +- *涯 +- *:-) + +(If your name is masked, please contact me via my homepage email to update it with your GitHub username.) + + + +================================================ +FILE: CLAUDE.md +================================================ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.You need use English to write text. + +## Key Development Commands +- Build: `npm run build` +- Start: `npm start` + +## Architecture +- Uses `express` for routing (see `src/server.ts`) +- Bundles with `esbuild` for CLI distribution +- Plugins are loaded from `$HOME/.claude-code-router/plugins` + + +================================================ +FILE: config.example.json +================================================ +{ + "Providers": [ + { + "name": "openrouter", + "api_base_url": "https://openrouter.ai/api/v1/chat/completions", + "api_key": "sk-xxx", + "models": [ + "google/gemini-2.5-pro-preview", + "anthropic/claude-sonnet-4", + "anthropic/claude-3.5-sonnet", + "anthropic/claude-3.7-sonnet:thinking" + ], + "transformer": { + "use": ["openrouter"] + } + }, + { + "name": "deepseek", + "api_base_url": "https://api.deepseek.com/chat/completions", + "api_key": "sk-xxx", + "models": ["deepseek-chat", "deepseek-reasoner"], + "transformer": { + "use": ["deepseek"], + "deepseek-chat": { + "use": ["tooluse"] + } + } + }, + { + "name": "ollama", + "api_base_url": "http://localhost:11434/v1/chat/completions", + "api_key": "ollama", + "models": ["qwen2.5-coder:latest"] + }, + { + "name": "gemini", + "api_base_url": "https://generativelanguage.googleapis.com/v1beta/models/", + "api_key": "sk-xxx", + "models": ["gemini-2.5-flash", "gemini-2.5-pro"], + "transformer": { + "use": ["gemini"] + } + }, + { + "name": "volcengine", + "api_base_url": "https://ark.cn-beijing.volces.com/api/v3/chat/completions", + "api_key": "sk-xxx", + "models": ["deepseek-v3-250324", "deepseek-r1-250528"], + "transformer": { + "use": ["deepseek"] + } + }, + { + "name": "siliconflow", + "api_base_url": "https://api.siliconflow.cn/v1/chat/completions", + "api_key": "sk-xxx", + "models": ["moonshotai/Kimi-K2-Instruct"], + "transformer": { + "use": [ + [ + "maxtoken", + { + "max_tokens": 16384 + } + ] + ] + } + } + ], + "Router": { + "default": "deepseek,deepseek-chat", + "background": "ollama,qwen2.5-coder:latest", + "think": "deepseek,deepseek-reasoner", + "longContext": "openrouter,google/gemini-2.5-pro-preview" + }, + "APIKEY": "your-secret-key", + "HOST": "0.0.0.0" +} + + + +================================================ +FILE: docker-compose.yml +================================================ +version: "3.8" + +services: + claude-code-reverse: + build: . + ports: + - "3456:3456" + environment: + - ENABLE_ROUTER=${ENABLE_ROUTER} + - OPENAI_API_KEY=${OPENAI_API_KEY} + - OPENAI_BASE_URL=${OPENAI_BASE_URL} + - OPENAI_MODEL=${OPENAI_MODEL} + restart: unless-stopped + + + +================================================ +FILE: dockerfile +================================================ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm i + +COPY . . + +EXPOSE 3456 + +CMD ["node", "index.mjs"] + + + +================================================ +FILE: LICENSE +================================================ +MIT License + +Copyright (c) 2025 musistudio + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +================================================ +FILE: package.json +================================================ +{ + "name": "@musistudio/claude-code-router", + "version": "1.0.21", + "description": "Use Claude Code without an Anthropics account and route it to another LLM provider", + "bin": { + "ccr": "./dist/cli.js" + }, + "scripts": { + "build": "esbuild src/cli.ts --bundle --platform=node --outfile=dist/cli.js && shx cp node_modules/tiktoken/tiktoken_bg.wasm dist/tiktoken_bg.wasm" + }, + "keywords": [ + "claude", + "code", + "router", + "llm", + "anthropic" + ], + "author": "musistudio", + "license": "MIT", + "dependencies": { + "@musistudio/llms": "^1.0.8", + "dotenv": "^16.4.7", + "tiktoken": "^1.0.21", + "uuid": "^11.1.0" + }, + "devDependencies": { + "esbuild": "^0.25.1", + "fastify": "^5.4.0", + "shx": "^0.4.0", + "typescript": "^5.8.2" + }, + "publishConfig": { + "ignore": [ + "!build/", + "src/", + "screenshots/" + ] + } +} + + + +================================================ +FILE: pnpm-lock.yaml +================================================ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@musistudio/llms': + specifier: ^1.0.8 + version: 1.0.8(ws@8.18.3)(zod@3.25.67) + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + tiktoken: + specifier: ^1.0.21 + version: 1.0.21 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + devDependencies: + esbuild: + specifier: ^0.25.1 + version: 0.25.5 + fastify: + specifier: ^5.4.0 + version: 5.4.0 + shx: + specifier: ^0.4.0 + version: 0.4.0 + typescript: + specifier: ^5.8.2 + version: 5.8.3 + +packages: + + '@anthropic-ai/sdk@0.54.0': + resolution: {integrity: sha512-xyoCtHJnt/qg5GG6IgK+UJEndz8h8ljzt/caKXmq3LfBF81nC/BW6E4x2rOWCZcvsLyVW+e8U5mtIr6UCE/kJw==} + hasBin: true + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fastify/ajv-compiler@4.0.2': + resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} + + '@fastify/cors@11.0.1': + resolution: {integrity: sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.0': + resolution: {integrity: sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.0.0': + resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} + + '@google/genai@1.8.0': + resolution: {integrity: sha512-n3KiMFesQCy2R9iSdBIuJ0JWYQ1HZBJJkmt4PPZMGZKvlgHhBAGw1kUMyX+vsAIzprN3lK45DI755lm70wPOOg==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.11.0 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@musistudio/llms@1.0.8': + resolution: {integrity: sha512-C2GFoiw/DEo2faAQerRVOyWEupTJpoV+3z3rE9XEN31ySOcsaVPnKyWPmKKg9EDMBw70gQg5FZFg3jZxSCnWlA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@9.1.0: + resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bignumber.js@9.3.0: + resolution: {integrity: sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + cross-spawn@6.0.6: + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stringify@6.0.1: + resolution: {integrity: sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fastify-plugin@5.0.1: + resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} + + fastify@5.4.0: + resolution: {integrity: sha512-I4dVlUe+WNQAhKSyv15w+dwUh2EPiEl4X2lGYMmNSgF83WzTMAPKGdWEv5tPsCQOb+SOZwz8Vlta2vF+OeDgRw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-my-way@9.3.0: + resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==} + engines: {node: '>=20'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-ref-resolver@2.0.1: + resolution: {integrity: sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openai@5.8.2: + resolution: {integrity: sha512-8C+nzoHYgyYOXhHGN6r0fcb4SznuEn1R7YZMvlqDbnCuE0FM2mm3T1HiYW6WIcMS/F1Of2up/cSPjLPaWt0X9Q==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.7.0: + resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} + hasBin: true + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + secure-json-parse@4.0.0: + resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shelljs@0.9.2: + resolution: {integrity: sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==} + engines: {node: '>=18'} + hasBin: true + + shx@0.4.0: + resolution: {integrity: sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==} + engines: {node: '>=18'} + hasBin: true + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + tiktoken@1.0.21: + resolution: {integrity: sha512-/kqtlepLMptX0OgbYD9aMYbM7EFrMZCL7EoHM8Psmg2FuhXoo/bH64KqOiZGGwa6oS9TPdSEDKBnV2LuB8+5vQ==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici@7.11.0: + resolution: {integrity: sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==} + engines: {node: '>=20.18.1'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + + zod@3.25.67: + resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} + +snapshots: + + '@anthropic-ai/sdk@0.54.0': {} + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@fastify/ajv-compiler@4.0.2': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.6 + + '@fastify/cors@11.0.1': + dependencies: + fastify-plugin: 5.0.1 + toad-cache: 3.7.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.0.1 + + '@fastify/forwarded@3.0.0': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.0.0': + dependencies: + '@fastify/forwarded': 3.0.0 + ipaddr.js: 2.2.0 + + '@google/genai@1.8.0': + dependencies: + google-auth-library: 9.15.1 + ws: 8.18.3 + zod: 3.25.67 + zod-to-json-schema: 3.24.6(zod@3.25.67) + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + '@musistudio/llms@1.0.8(ws@8.18.3)(zod@3.25.67)': + dependencies: + '@anthropic-ai/sdk': 0.54.0 + '@fastify/cors': 11.0.1 + '@google/genai': 1.8.0 + dotenv: 16.6.1 + fastify: 5.4.0 + openai: 5.8.2(ws@8.18.3)(zod@3.25.67) + undici: 7.11.0 + uuid: 11.1.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - encoding + - supports-color + - utf-8-validate + - ws + - zod + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + abstract-logging@2.0.1: {} + + agent-base@7.1.3: {} + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + atomic-sleep@1.0.0: {} + + avvio@9.1.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + + base64-js@1.5.1: {} + + bignumber.js@9.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-equal-constant-time@1.0.1: {} + + cookie@1.0.2: {} + + cross-spawn@6.0.6: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + dequal@2.0.3: {} + + dotenv@16.6.1: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + execa@1.0.0: + dependencies: + cross-spawn: 6.0.6 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + extend@3.0.2: {} + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stringify@6.0.1: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.6 + json-schema-ref-resolver: 2.0.1 + rfdc: 1.4.1 + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-redact@3.5.0: {} + + fast-uri@3.0.6: {} + + fastify-plugin@5.0.1: {} + + fastify@5.4.0: + dependencies: + '@fastify/ajv-compiler': 4.0.2 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.0.0 + abstract-logging: 2.0.1 + avvio: 9.1.0 + fast-json-stringify: 6.0.1 + find-my-way: 9.3.0 + light-my-request: 6.6.0 + pino: 9.7.0 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.0.0 + semver: 7.7.2 + toad-cache: 3.7.0 + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-my-way@9.3.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.0.0 + + function-bind@1.1.2: {} + + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + get-stream@4.1.0: + dependencies: + pump: 3.0.3 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + interpret@1.4.0: {} + + ipaddr.js@2.2.0: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-stream@1.1.0: {} + + is-stream@2.0.1: {} + + isexe@2.0.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.0 + + json-schema-ref-resolver@2.0.1: + dependencies: + dequal: 2.0.3 + + json-schema-traverse@1.0.0: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + light-my-request@6.6.0: + dependencies: + cookie: 1.0.2 + process-warning: 4.0.1 + set-cookie-parser: 2.7.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nice-try@1.0.5: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + on-exit-leak-free@2.1.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openai@5.8.2(ws@8.18.3)(zod@3.25.67): + optionalDependencies: + ws: 8.18.3 + zod: 3.25.67 + + p-finally@1.0.0: {} + + path-key@2.0.1: {} + + path-parse@1.0.7: {} + + picomatch@2.3.1: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.7.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + queue-microtask@1.2.3: {} + + quick-format-unescaped@4.0.4: {} + + real-require@0.2.0: {} + + rechoir@0.6.2: + dependencies: + resolve: 1.22.10 + + require-from-string@2.0.2: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + ret@0.5.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safe-regex2@5.0.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + + secure-json-parse@4.0.0: {} + + semver@5.7.2: {} + + semver@7.7.2: {} + + set-cookie-parser@2.7.1: {} + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-regex@1.0.0: {} + + shelljs@0.9.2: + dependencies: + execa: 1.0.0 + fast-glob: 3.3.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + shx@0.4.0: + dependencies: + minimist: 1.2.8 + shelljs: 0.9.2 + + signal-exit@3.0.7: {} + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + + split2@4.2.0: {} + + strip-eof@1.0.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + tiktoken@1.0.21: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toad-cache@3.7.0: {} + + tr46@0.0.3: {} + + typescript@5.8.3: {} + + undici@7.11.0: {} + + uuid@11.1.0: {} + + uuid@9.0.1: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + ws@8.18.3: {} + + zod-to-json-schema@3.24.6(zod@3.25.67): + dependencies: + zod: 3.25.67 + + zod@3.25.67: {} + + + +================================================ +FILE: README_zh.md +================================================ +# Claude Code Router + +> 一款强大的工具,可将 Claude Code 请求路由到不同的模型,并自定义任何请求。 + +![](blog/images/claude-code.png) + +## ✨ 功能 + +- **模型路由**: 根据您的需求将请求路由到不同的模型(例如,后台任务、思考、长上下文)。 +- **多提供商支持**: 支持 OpenRouter、DeepSeek、Ollama、Gemini、Volcengine 和 SiliconFlow 等各种模型提供商。 +- **请求/响应转换**: 使用转换器为不同的提供商自定义请求和响应。 +- **动态模型切换**: 在 Claude Code 中使用 `/model` 命令动态切换模型。 +- **GitHub Actions 集成**: 在您的 GitHub 工作流程中触发 Claude Code 任务。 +- **插件系统**: 使用自定义转换器扩展功能。 + +## 🚀 快速入门 + +### 1. 安装 + +首先,请确保您已安装 [Claude Code](https://docs.anthropic.com/en/docs/claude-code/quickstart): + +```shell +npm install -g @anthropic-ai/claude-code +``` + +然后,安装 Claude Code Router: + +```shell +npm install -g @musistudio/claude-code-router +``` + +### 2. 配置 + +创建并配置您的 `~/.claude-code-router/config.json` 文件。有关更多详细信息,您可以参考 `config.example.json`。 + +`config.json` 文件有几个关键部分: +- **`PROXY_URL`** (可选): 您可以为 API 请求设置代理,例如:`"PROXY_URL": "http://127.0.0.1:7890"`。 +- **`LOG`** (可选): 您可以通过将其设置为 `true` 来启用日志记录。日志文件将位于 `$HOME/.claude-code-router.log`。 +- **`APIKEY`** (可选): 您可以设置一个密钥来进行身份验证。设置后,客户端请求必须在 `Authorization` 请求头 (例如, `Bearer your-secret-key`) 或 `x-api-key` 请求头中提供此密钥。例如:`"APIKEY": "your-secret-key"`。 +- **`HOST`** (可选): 您可以设置服务的主机地址。如果未设置 `APIKEY`,出于安全考虑,主机地址将强制设置为 `127.0.0.1`,以防止未经授权的访问。例如:`"HOST": "0.0.0.0"`。 +- **`Providers`**: 用于配置不同的模型提供商。 +- **`Router`**: 用于设置路由规则。`default` 指定默认模型,如果未配置其他路由,则该模型将用于所有请求。 + +这是一个综合示例: + +```json +{ + "APIKEY": "your-secret-key", + "PROXY_URL": "http://127.0.0.1:7890", + "LOG": true, + "Providers": [ + { + "name": "openrouter", + "api_base_url": "https://openrouter.ai/api/v1/chat/completions", + "api_key": "sk-xxx", + "models": [ + "google/gemini-2.5-pro-preview", + "anthropic/claude-sonnet-4", + "anthropic/claude-3.5-sonnet" + ], + "transformer": { "use": ["openrouter"] } + }, + { + "name": "deepseek", + "api_base_url": "https://api.deepseek.com/chat/completions", + "api_key": "sk-xxx", + "models": ["deepseek-chat", "deepseek-reasoner"], + "transformer": { + "use": ["deepseek"], + "deepseek-chat": { "use": ["tooluse"] } + } + }, + { + "name": "ollama", + "api_base_url": "http://localhost:11434/v1/chat/completions", + "api_key": "ollama", + "models": ["qwen2.5-coder:latest"] + } + ], + "Router": { + "default": "deepseek,deepseek-chat", + "background": "ollama,qwen2.5-coder:latest", + "think": "deepseek,deepseek-reasoner", + "longContext": "openrouter,google/gemini-2.5-pro-preview" + } +} +``` + + +### 3. 使用 Router 运行 Claude Code + +使用 router 启动 Claude Code: + +```shell +ccr code +``` + +#### Providers + +`Providers` 数组是您定义要使用的不同模型提供商的地方。每个提供商对象都需要: + +- `name`: 提供商的唯一名称。 +- `api_base_url`: 聊天补全的完整 API 端点。 +- `api_key`: 您提供商的 API 密钥。 +- `models`: 此提供商可用的模型名称列表。 +- `transformer` (可选): 指定用于处理请求和响应的转换器。 + +#### Transformers + +Transformers 允许您修改请求和响应负载,以确保与不同提供商 API 的兼容性。 + +- **全局 Transformer**: 将转换器应用于提供商的所有模型。在此示例中,`openrouter` 转换器将应用于 `openrouter` 提供商下的所有模型。 + ```json + { + "name": "openrouter", + "api_base_url": "https://openrouter.ai/api/v1/chat/completions", + "api_key": "sk-xxx", + "models": [ + "google/gemini-2.5-pro-preview", + "anthropic/claude-sonnet-4", + "anthropic/claude-3.5-sonnet" + ], + "transformer": { "use": ["openrouter"] } + } + ``` +- **特定于模型的 Transformer**: 将转换器应用于特定模型。在此示例中,`deepseek` 转换器应用于所有模型,而额外的 `tooluse` 转换器仅应用于 `deepseek-chat` 模型。 + ```json + { + "name": "deepseek", + "api_base_url": "https://api.deepseek.com/chat/completions", + "api_key": "sk-xxx", + "models": ["deepseek-chat", "deepseek-reasoner"], + "transformer": { + "use": ["deepseek"], + "deepseek-chat": { "use": ["tooluse"] } + } + } + ``` + +- **向 Transformer 传递选项**: 某些转换器(如 `maxtoken`)接受选项。要传递选项,请使用嵌套数组,其中第一个元素是转换器名称,第二个元素是选项对象。 + ```json + { + "name": "siliconflow", + "api_base_url": "https://api.siliconflow.cn/v1/chat/completions", + "api_key": "sk-xxx", + "models": ["moonshotai/Kimi-K2-Instruct"], + "transformer": { + "use": [ + [ + "maxtoken", + { + "max_tokens": 16384 + } + ] + ] + } + } + ``` + +**可用的内置 Transformer:** + +- `deepseek`: 适配 DeepSeek API 的请求/响应。 +- `gemini`: 适配 Gemini API 的请求/响应。 +- `openrouter`: 适配 OpenRouter API 的请求/响应。 +- `groq`: 适配 groq API 的请求/响应 +- `maxtoken`: 设置特定的 `max_tokens` 值。 +- `tooluse`: 优化某些模型的工具使用(通过`tool_choice`参数)。 +- `gemini-cli` (实验性): 通过 Gemini CLI [gemini-cli.js](https://gist.github.com/musistudio/1c13a65f35916a7ab690649d3df8d1cd) 对 Gemini 的非官方支持。 + +**自定义 Transformer:** + +您还可以创建自己的转换器,并通过 `config.json` 中的 `transformers` 字段加载它们。 + +```json +{ + "transformers": [ + { + "path": "$HOME/.claude-code-router/plugins/gemini-cli.js", + "options": { + "project": "xxx" + } + } + ] +} +``` + +#### Router + +`Router` 对象定义了在不同场景下使用哪个模型: + +- `default`: 用于常规任务的默认模型。 +- `background`: 用于后台任务的模型。这可以是一个较小的本地模型以节省成本。 +- `think`: 用于推理密集型任务(如计划模式)的模型。 +- `longContext`: 用于处理长上下文(例如,> 60K 令牌)的模型。 + +您还可以使用 `/model` 命令在 Claude Code 中动态切换模型: +`/model provider_name,model_name` +示例: `/model openrouter,anthropic/claude-3.5-sonnet` + + +## 🤖 GitHub Actions + +将 Claude Code Router 集成到您的 CI/CD 管道中。在设置 [Claude Code Actions](https://docs.anthropic.com/en/docs/claude-code/github-actions) 后,修改您的 `.github/workflows/claude.yaml` 以使用路由器: + +```yaml +name: Claude Code + +on: + issue_comment: + types: [created] + # ... other triggers + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + # ... other conditions + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Prepare Environment + run: | + curl -fsSL https://bun.sh/install | bash + mkdir -p $HOME/.claude-code-router + cat << 'EOF' > $HOME/.claude-code-router/config.json + { + "log": true, + "OPENAI_API_KEY": "${{ secrets.OPENAI_API_KEY }}", + "OPENAI_BASE_URL": "https://api.deepseek.com", + "OPENAI_MODEL": "deepseek-chat" + } + EOF + shell: bash + + - name: Start Claude Code Router + run: | + nohup ~/.bun/bin/bunx @musistudio/claude-code-router@1.0.8 start & + shell: bash + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@beta + env: + ANTHROPIC_BASE_URL: http://localhost:3456 + with: + anthropic_api_key: "any-string-is-ok" +``` + +这种设置可以实现有趣的自动化,例如在非高峰时段运行任务以降低 API 成本。 + +## 📝 深入阅读 + +- [项目动机和工作原理](blog/zh/项目初衷及原理.md) +- [也许我们可以用路由器做更多事情](blog/zh/或许我们能在Router中做更多事情.md) + +## ❤️ 支持与赞助 + +如果您觉得这个项目有帮助,请考虑赞助它的开发。非常感谢您的支持! + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM) + + + + + + +
AlipayWeChat Pay
+ +### 我们的赞助商 + +非常感谢所有赞助商的慷慨支持! + +- @Simon Leischnig +- [@duanshuaimin](https://github.com/duanshuaimin) +- [@vrgitadmin](https://github.com/vrgitadmin) +- @*o +- [@ceilwoo](https://github.com/ceilwoo) +- @*说 +- @*更 +- @K*g +- @R*R +- [@bobleer](https://github.com/bobleer) +- @*苗 +- @*划 +- [@Clarence-pan](https://github.com/Clarence-pan) +- [@carter003](https://github.com/carter003) +- @S*r +- @*晖 +- @*敏 +- @Z*z +- @*然 +- [@cluic](https://github.com/cluic) +- @*苗 +- [@PromptExpert](https://github.com/PromptExpert) +- @*应 +- [@yusnake](https://github.com/yusnake) +- @*飞 +- @董* +- *汀 +- *涯 +- *:-) + +(如果您的名字被屏蔽,请通过我的主页电子邮件与我联系,以便使用您的 GitHub 用户名进行更新。) + + +## 交流群 +wechat_group + + +================================================ +FILE: tsconfig.json +================================================ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "noImplicitAny": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "declaration": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} + + + +================================================ +FILE: .dockerignore +================================================ +node_modules +npm-debug.log + + + +================================================ +FILE: .npmignore +================================================ +src +node_modules +.claude +CLAUDE.md +screenshoots +.DS_Store +.vscode +.idea +.env +.blog +docs +.log +blog +config.json + + + +================================================ +FILE: blog/en/maybe-we-can-do-more-with-the-route.md +================================================ +# Maybe We Can Do More with the Router + +Since the release of `claude-code-router`, I’ve received a lot of user feedback, and quite a few issues are still open. Most of them are related to support for different providers and the lack of tool usage from the deepseek model. + +Originally, I created this project for personal use, mainly to access claude code at a lower cost. So, multi-provider support wasn’t part of the initial design. But during troubleshooting, I discovered that even though most providers claim to be compatible with the OpenAI-style `/chat/completions` interface, there are many subtle differences. For example: + +1. When Gemini's tool parameter type is string, the `format` field only supports `date` and `date-time`, and there’s no tool call ID. + +2. OpenRouter requires `cache_control` for caching. + +3. The official DeepSeek API has a `max_output` of 8192, but Volcano Engine’s limit is even higher. + +Aside from these, smaller providers often have quirks in their parameter handling. So I decided to create a new project, [musistudio/llms](https://github.com/musistudio/llms), to deal with these compatibility issues. It uses the OpenAI format as a base and introduces a generic Transformer interface for transforming both requests and responses. + +Once a `Transformer` is implemented for each provider, it becomes possible to mix-and-match requests between them. For example, I implemented bidirectional conversion between Anthropic and OpenAI formats in `AnthropicTransformer`, which listens to the `/v1/messages` endpoint. Similarly, `GeminiTransformer` handles Gemini <-> OpenAI format conversions and listens to `/v1beta/models/:modelAndAction`. + +When both requests and responses are transformed into a common format, they can interoperate seamlessly: + +``` +AnthropicRequest -> AnthropicTransformer -> OpenAIRequest -> GeminiTransformer -> GeminiRequest -> GeminiServer +``` + +``` +GeminiResponse -> GeminiTransformer -> OpenAIResponse -> AnthropicTransformer -> AnthropicResponse +``` + +Using a middleware layer to smooth out differences may introduce some performance overhead, but the main goal here is to enable `claude-code-router` to support multiple providers. + +As for the issue of DeepSeek’s lackluster tool usage — I found that it stems from poor instruction adherence in long conversations. Initially, the model actively calls tools, but after several rounds, it starts responding with plain text instead. My first workaround was injecting a system prompt to remind the model to use tools proactively. But in long contexts, the model tends to forget this instruction. + +After reading the DeepSeek documentation, I noticed it supports the `tool_choice` parameter, which can be set to `"required"` to force the model to use at least one tool. I tested this by enabling the parameter, and it significantly improved the model’s tool usage. We can remove the setting when it's no longer necessary. With the help of the `Transformer` interface in [musistudio/llms](https://github.com/musistudio/llms), we can modify the request before it’s sent and adjust the response after it’s received. + +Inspired by the Plan Mode in `claude code`, I implemented a similar Tool Mode for DeepSeek: + +```typescript +export class TooluseTransformer implements Transformer { + name = "tooluse"; + + transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest { + if (request.tools?.length) { + request.messages.push({ + role: "system", + content: `Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task. +Before invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \`ExitTool\` to exit tool mode — this is the only valid way to terminate tool mode. +Always prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.`, + }); + request.tool_choice = "required"; + request.tools.unshift({ + type: "function", + function: { + name: "ExitTool", + description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode. +IMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode. +Examples: +1. Task: "Use a tool to summarize this document" — Do not use ExitTool if a summarization tool is available. +2. Task: "What’s the weather today?" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`, + parameters: { + type: "object", + properties: { + response: { + type: "string", + description: + "Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.", + }, + }, + required: ["response"], + }, + }, + }); + } + return request; + } + + async transformResponseOut(response: Response): Promise { + if (response.headers.get("Content-Type")?.includes("application/json")) { + const jsonResponse = await response.json(); + if ( + jsonResponse?.choices[0]?.message.tool_calls?.length && + jsonResponse?.choices[0]?.message.tool_calls[0]?.function?.name === + "ExitTool" + ) { + const toolArguments = JSON.parse(toolCall.function.arguments || "{}"); + jsonResponse.choices[0].message.content = toolArguments.response || ""; + delete jsonResponse.choices[0].message.tool_calls; + } + + // Handle non-streaming response if needed + return new Response(JSON.stringify(jsonResponse), { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + } else if (response.headers.get("Content-Type")?.includes("stream")) { + // ... + } + return response; + } +} +``` + +This transformer ensures the model calls at least one tool. If no tools are appropriate or the task is finished, it can exit using `ExitTool`. Since this relies on the `tool_choice` parameter, it only works with models that support it. + +In practice, this approach noticeably improves tool usage for DeepSeek. The tradeoff is that sometimes the model may invoke irrelevant or unnecessary tools, which could increase latency and token usage. + +This update is just a small experiment — adding an `“agent”` to the router. Maybe there are more interesting things we can explore from here. + + +================================================ +FILE: blog/en/project-motivation-and-how-it-works.md +================================================ +# Project Motivation and Principles + +As early as the day after Claude Code was released (2025-02-25), I began and completed a reverse engineering attempt of the project. At that time, using Claude Code required registering for an Anthropic account, applying for a waitlist, and waiting for approval. However, due to well-known reasons, Anthropic blocks users from mainland China, making it impossible for me to use the service through normal means. Based on known information, I discovered the following: + +1. Claude Code is installed via npm, so it's very likely developed with Node.js. +2. Node.js offers various debugging methods: simple `console.log` usage, launching with `--inspect` to hook into Chrome DevTools, or even debugging obfuscated code using `d8`. + +My goal was to use Claude Code without an Anthropic account. I didn’t need the full source code—just a way to intercept and reroute requests made by Claude Code to Anthropic’s models to my own custom endpoint. So I started the reverse engineering process: + +1. First, install Claude Code: +```bash +npm install -g @anthropic-ai/claude-code +``` + +2. After installation, the project is located at `~/.nvm/versions/node/v20.10.0/lib/node_modules/@anthropic-ai/claude-code`(this may vary depending on your Node version manager and version). + +3. Open the package.json to analyze the entry point: +```package.json +{ + "name": "@anthropic-ai/claude-code", + "version": "1.0.24", + "main": "sdk.mjs", + "types": "sdk.d.ts", + "bin": { + "claude": "cli.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "type": "module", + "author": "Boris Cherny ", + "license": "SEE LICENSE IN README.md", + "description": "Use Claude, Anthropic's AI assistant, right from your terminal. Claude can understand your codebase, edit files, run terminal commands, and handle entire workflows for you.", + "homepage": "https://github.com/anthropics/claude-code", + "bugs": { + "url": "https://github.com/anthropics/claude-code/issues" + }, + "scripts": { + "prepare": "node -e \"if (!process.env.AUTHORIZED) { console.error('ERROR: Direct publishing is not allowed.\\nPlease use the publish-external.sh script to publish this package.'); process.exit(1); }\"", + "preinstall": "node scripts/preinstall.js" + }, + "dependencies": {}, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "^0.33.5", + "@img/sharp-darwin-x64": "^0.33.5", + "@img/sharp-linux-arm": "^0.33.5", + "@img/sharp-linux-arm64": "^0.33.5", + "@img/sharp-linux-x64": "^0.33.5", + "@img/sharp-win32-x64": "^0.33.5" + } +} +``` + +The key entry is `"claude": "cli.js"`. Opening cli.js, you'll see the code is minified and obfuscated. But using WebStorm’s `Format File` feature, you can reformat it for better readability: +![webstorm-formate-file](../images/webstorm-formate-file.png) + +Now you can begin understanding Claude Code’s internal logic and prompt structure by reading the code. To dig deeper, you can insert console.log statements or launch in debug mode with Chrome DevTools using: + +```bash +NODE_OPTIONS="--inspect-brk=9229" claude +``` + +This command starts Claude Code in debug mode and opens port 9229. Visit chrome://inspect/ in Chrome and click inspect to begin debugging: +![chrome-devtools](../images/chrome-inspect.png) +![chrome-devtools](../images/chrome-devtools.png) + +By searching for the keyword api.anthropic.com, you can easily locate where Claude Code makes its API calls. From the surrounding code, it's clear that baseURL can be overridden with the `ANTHROPIC_BASE_URL` environment variable, and `apiKey` and `authToken` can be configured similarly: +![search](../images/search.png) + +So far, we’ve discovered some key information: + +1. Environment variables can override Claude Code's `baseURL` and `apiKey`. + +2. Claude Code adheres to the Anthropic API specification. + +Therefore, we need: +1. A service to convert OpenAI API–compatible requests into Anthropic API format. + +2. Set the environment variables before launching Claude Code to redirect requests to this service. + +Thus, `claude-code-router` was born. This project uses `Express.js` to implement the `/v1/messages` endpoint. It leverages middlewares to transform request/response formats and supports request rewriting (useful for prompt tuning per model). + +Back in February, the full DeepSeek model series had poor support for Function Calling, so I initially used `qwen-max`. It worked well—but without KV cache support, it consumed a large number of tokens and couldn’t provide the native `Claude Code` experience. + +So I experimented with a Router-based mode using a lightweight model to dispatch tasks. The architecture included four roles: `router`, `tool`, `think`, and `coder`. Each request passed through a free lightweight model that would decide whether the task involved reasoning, coding, or tool usage. Reasoning and coding tasks looped until a tool was invoked to apply changes. However, the lightweight model lacked the capability to route tasks accurately, and architectural issues prevented it from effectively driving Claude Code. + +Everything changed at the end of May when the official Claude Code was launched, and `DeepSeek-R1` model (released 2025-05-28) added Function Call support. I redesigned the system. With the help of AI pair programming, I fixed earlier request/response transformation issues—especially the handling of models that return JSON instead of Function Call outputs. + +This time, I used the `DeepSeek-V3` model. It performed better than expected: supporting most tool calls, handling task decomposition and stepwise planning, and—most importantly—costing less than one-tenth the price of Claude 3.5 Sonnet. + +The official Claude Code organizes agents differently from the beta version, so I restructured my Router mode to include four roles: the default model, `background`, `think`, and `longContext`. + +- The default model handles general tasks and acts as a fallback. + +- The `background` model manages lightweight background tasks. According to Anthropic, Claude Haiku 3.5 is often used here, so I routed this to a local `ollama` service. + +- The `think` model is responsible for reasoning and planning mode tasks. I use `DeepSeek-R1` here, though it doesn’t support cost control, so `Think` and `UltraThink` behave identically. + +- The `longContext` model handles long-context scenarios. The router uses `tiktoken` to calculate token lengths in real time, and if the context exceeds 32K, it switches to this model to compensate for DeepSeek's long-context limitations. + +This describes the evolution and reasoning behind the project. By cleverly overriding environment variables, we can forward and modify requests without altering Claude Code’s source—allowing us to benefit from official updates while using our own models and custom prompts. + +This project offers a practical approach to running Claude Code under Anthropic’s regional restrictions, balancing `cost`, `performance`, and `customizability`. That said, the official `Max Plan` still offers the best experience if available. + + +================================================ +FILE: blog/zh/或许我们能在Router中做更多事情.md +================================================ +[Binary file] + + +================================================ +FILE: blog/zh/项目初衷及原理.md +================================================ +# 项目初衷及原理 + +早在 Claude Code 发布的第二天(2025-02-25),我就尝试并完成了对该项目的逆向。当时要使用 Claude Code 你需要注册一个 Anthropic 账号,然后申请 waitlist,等待通过后才能使用。但是因为众所周知的原因,Anthropic 屏蔽了中国区的用户,所以通过正常手段我无法使用,通过已知的信息,我发现: + +1. Claude Code 使用 npm 进行安装,所以很大可能其使用 Node.js 进行开发。 +2. Node.js 调试手段众多,可以简单使用`console.log`获取想要的信息,也可以使用`--inspect`将其接入`Chrome Devtools`,甚至你可以使用`d8`去调试某些加密混淆的代码。 + +由于我的目标是让我在没有 Anthropic 账号的情况下使用`Claude Code`,我并不需要获得完整的源代码,只需要将`Claude Code`请求 Anthropic 模型时将其转发到我自定义的接口即可。接下来我就开启了我的逆向过程: + +1. 首先安装`Claude Code` + +```bash +npm install -g @anthropic-ai/claude-code +``` + +2. 安装后该项目被放在了`~/.nvm/versions/node/v20.10.0/lib/node_modules/@anthropic-ai/claude-code`中,因为我使用了`nvm`作为我的 node 版本控制器,当前使用`node-v20.10.0`,所以该路径会因人而异。 +3. 找到项目路径之后可通过 package.json 分析包入口,内容如下: + +```package.json +{ + "name": "@anthropic-ai/claude-code", + "version": "1.0.24", + "main": "sdk.mjs", + "types": "sdk.d.ts", + "bin": { + "claude": "cli.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "type": "module", + "author": "Boris Cherny ", + "license": "SEE LICENSE IN README.md", + "description": "Use Claude, Anthropic's AI assistant, right from your terminal. Claude can understand your codebase, edit files, run terminal commands, and handle entire workflows for you.", + "homepage": "https://github.com/anthropics/claude-code", + "bugs": { + "url": "https://github.com/anthropics/claude-code/issues" + }, + "scripts": { + "prepare": "node -e \"if (!process.env.AUTHORIZED) { console.error('ERROR: Direct publishing is not allowed.\\nPlease use the publish-external.sh script to publish this package.'); process.exit(1); }\"", + "preinstall": "node scripts/preinstall.js" + }, + "dependencies": {}, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "^0.33.5", + "@img/sharp-darwin-x64": "^0.33.5", + "@img/sharp-linux-arm": "^0.33.5", + "@img/sharp-linux-arm64": "^0.33.5", + "@img/sharp-linux-x64": "^0.33.5", + "@img/sharp-win32-x64": "^0.33.5" + } +} +``` + +其中`"claude": "cli.js"`就是我们要找的入口,打开 cli.js,发现代码被压缩混淆过了。没关系,借助`webstorm`的`Formate File`功能可以重新格式化,让代码变得稍微好看一点。就像这样: +![webstorm-formate-file](../images/webstorm-formate-file.png) + +现在,你可以通过阅读部分代码来了解`Claude Code`的内容工具原理与提示词。你也可以在关键地方使用`console.log`来获得更多信息,当然,也可以使用`Chrome Devtools`来进行断点调试,使用以下命令启动`Claude Code`: + +```bash +NODE_OPTIONS="--inspect-brk=9229" claude +``` + +该命令会以调试模式启动`Claude Code`,并将调试的端口设置为`9229`。这时候通过 Chrome 访问`chrome://inspect/`即可看到当前的`Claude Code`进程,点击`inspect`即可进行调试。 +![chrome-devtools](../images/chrome-inspect.png) +![chrome-devtools](../images/chrome-devtools.png) + +通过搜索关键字符`api.anthropic.com`很容易能找到`Claude Code`用来发请求的地方,根据上下文的查看,很容易发现这里的`baseURL`可以通过环境变量`ANTHROPIC_BASE_URL`进行覆盖,`apiKey`和`authToken`也同理。 +![search](../images/search.png) + +到目前为止,我们获得关键信息: + +1. 可以使用环境变量覆盖`Claude Code`的`BaseURL`和`apiKey`的配置 + +2. `Claude Code`使用[Anthropic API](https://docs.anthropic.com/en/api/overview)的规范 + +所以我们需要: + +1. 实现一个服务用来将`OpenAI API`的规范转换成`Anthropic API`格式。 + +2. 启动`Claude Code`之前写入环境变量将`baseURL`指向到该服务。 + +于是,`claude-code-router`就诞生了,该项目使用`Express.js`作为 HTTP 服务,实现`/v1/messages`端点,使用`middlewares`处理请求/响应的格式转换以及请求重写功能(可以用来重写 Claude Code 的提示词以针对单个模型进行调优)。 +在 2 月份由于`DeepSeek`全系列模型对`Function Call`的支持不佳导致无法直接使用`DeepSeek`模型,所以在当时我选择了`qwen-max`模型,一切表现的都很好,但是`qwen-max`不支持`KV Cache`,意味着我要消耗大量的 token,但是却无法获取`Claude Code`原生的体验。 +所以我又尝试了`Router`模式,即使用一个小模型对任务进行分发,一共分为四个模型:`router`、`tool`、`think`和`coder`,所有的请求先经过一个免费的小模型,由小模型去判断应该是进行思考还是编码还是调用工具,再进行任务的分发,如果是思考和编码任务将会进行循环调用,直到最终使用工具写入或修改文件。但是实践下来发现免费的小模型不足以很好的完成任务的分发,再加上整个 Agnet 的设计存在缺陷,导致并不能很好的驱动`Claude Code`。 +直到 5 月底,`Claude Code`被正式推出,这时`DeepSeek`全系列模型(R1 于 05-28)均支持`Function Call`,我开始重新设计该项目。在与 AI 的结对编程中我修复了之前的请求和响应转换问题,在某些场景下模型输出 JSON 响应而不是`Function Call`。这次直接使用`DeepSeek-v3`模型,它工作的比我想象中要好:能完成绝大多数工具调用,还支持用步骤规划解决任务,最关键的是`DeepSeek`的价格不到`claude Sonnet 3.5`的十分之一。正式发布的`Claude Code`对 Agent 的组织也不同于测试版,于是在分析了`Claude Code`的请求调用之后,我重新组织了`Router`模式:现在它还是四个模型:默认模型、`background`、`think`和`longContext`。 + +- 默认模型作为最终的兜底和日常处理 + +- `background`是用来处理一些后台任务,据 Anthropic 官方说主要用`Claude Haiku 3.5`模型去处理一些小任务,如俳句生成和对话摘要,于是我将其路由到了本地的`ollama`服务。 + +- `think`模型用于让`Claude Code`进行思考或者在`Plan Mode`下使用,这里我使用的是`DeepSeek-R1`,由于其不支持推理成本控制,所以`Think`和`UltraThink`是一样的逻辑。 + +- `longContext`是用于处理长下上文的场景,该项目会对每次请求使用tiktoken实时计算上下文长度,如果上下文大于32K则使用该模型,旨在弥补`DeepSeek`在长上下文处理不佳的情况。 + +以上就是该项目的发展历程以及我的一些思考,通过巧妙的使用环境变量覆盖的手段在不修改`Claude Code`源码的情况下完成请求的转发和修改,这就使得在可以得到 Anthropic 更新的同时使用自己的模型,自定义自己的提示词。该项目只是在 Anthropic 封禁中国区用户的情况下使用`Claude Code`并且达到成本和性能平衡的一种手段。如果可以的话,还是官方的Max Plan体验最好。 + + + +================================================ +FILE: src/cli.ts +================================================ +#!/usr/bin/env node +import { run } from "./index"; +import { showStatus } from "./utils/status"; +import { executeCodeCommand } from "./utils/codeCommand"; +import { cleanupPidFile, isServiceRunning } from "./utils/processCheck"; +import { version } from "../package.json"; +import { spawn } from "child_process"; +import { PID_FILE, REFERENCE_COUNT_FILE } from "./constants"; +import { existsSync, readFileSync } from "fs"; +import {join} from "path"; + +const command = process.argv[2]; + +const HELP_TEXT = ` +Usage: ccr [command] + +Commands: + start Start service + stop Stop service + status Show service status + code Execute code command + -v, version Show version information + -h, help Show help information + +Example: + ccr start + ccr code "Write a Hello World" +`; + +async function waitForService( + timeout = 10000, + initialDelay = 1000 +): Promise { + // Wait for an initial period to let the service initialize + await new Promise((resolve) => setTimeout(resolve, initialDelay)); + + const startTime = Date.now(); + while (Date.now() - startTime < timeout) { + if (isServiceRunning()) { + // Wait for an additional short period to ensure service is fully ready + await new Promise((resolve) => setTimeout(resolve, 500)); + return true; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + return false; +} + +async function main() { + switch (command) { + case "start": + run(); + break; + case "stop": + try { + const pid = parseInt(readFileSync(PID_FILE, "utf-8")); + process.kill(pid); + cleanupPidFile(); + if (existsSync(REFERENCE_COUNT_FILE)) { + try { + require("fs").unlinkSync(REFERENCE_COUNT_FILE); + } catch (e) { + // Ignore cleanup errors + } + } + console.log( + "claude code router service has been successfully stopped." + ); + } catch (e) { + console.log( + "Failed to stop the service. It may have already been stopped." + ); + cleanupPidFile(); + } + break; + case "status": + showStatus(); + break; + case "code": + if (!isServiceRunning()) { + console.log("Service not running, starting service..."); + const cliPath = join(__dirname, "cli.js"); + const startProcess = spawn("node", [cliPath, "start"], { + detached: true, + stdio: "ignore", + }); + + startProcess.on("error", (error) => { + console.error("Failed to start service:", error); + process.exit(1); + }); + + startProcess.unref(); + + if (await waitForService()) { + executeCodeCommand(process.argv.slice(3)); + } else { + console.error( + "Service startup timeout, please manually run `ccr start` to start the service" + ); + process.exit(1); + } + } else { + executeCodeCommand(process.argv.slice(3)); + } + break; + case "-v": + case "version": + console.log(`claude-code-router version: ${version}`); + break; + case "-h": + case "help": + console.log(HELP_TEXT); + break; + default: + console.log(HELP_TEXT); + process.exit(1); + } +} + +main().catch(console.error); + + + +================================================ +FILE: src/constants.ts +================================================ +import path from "node:path"; +import os from "node:os"; + +export const HOME_DIR = path.join(os.homedir(), ".claude-code-router"); + +export const CONFIG_FILE = path.join(HOME_DIR, "config.json"); + +export const PLUGINS_DIR = path.join(HOME_DIR, "plugins"); + +export const PID_FILE = path.join(HOME_DIR, '.claude-code-router.pid'); + +export const REFERENCE_COUNT_FILE = path.join(os.tmpdir(), "claude-code-reference-count.txt"); + + +export const DEFAULT_CONFIG = { + LOG: false, + OPENAI_API_KEY: "", + OPENAI_BASE_URL: "", + OPENAI_MODEL: "", +}; + + + +================================================ +FILE: src/index.ts +================================================ +import { existsSync } from "fs"; +import { writeFile } from "fs/promises"; +import { homedir } from "os"; +import { join } from "path"; +import { initConfig, initDir } from "./utils"; +import { createServer } from "./server"; +import { router } from "./utils/router"; +import { apiKeyAuth } from "./middleware/auth"; +import { + cleanupPidFile, + isServiceRunning, + savePid, +} from "./utils/processCheck"; +import { CONFIG_FILE } from "./constants"; + +async function initializeClaudeConfig() { + const homeDir = homedir(); + const configPath = join(homeDir, ".claude.json"); + if (!existsSync(configPath)) { + const userID = Array.from( + { length: 64 }, + () => Math.random().toString(16)[2] + ).join(""); + const configContent = { + numStartups: 184, + autoUpdaterStatus: "enabled", + userID, + hasCompletedOnboarding: true, + lastOnboardingVersion: "1.0.17", + projects: {}, + }; + await writeFile(configPath, JSON.stringify(configContent, null, 2)); + } +} + +interface RunOptions { + port?: number; +} + +async function run(options: RunOptions = {}) { + // Check if service is already running + if (isServiceRunning()) { + console.log("✅ Service is already running in the background."); + return; + } + + await initializeClaudeConfig(); + await initDir(); + const config = await initConfig(); + let HOST = config.HOST; + + if (config.HOST && !config.APIKEY) { + HOST = "127.0.0.1"; + console.warn( + "⚠️ API key is not set. HOST is forced to 127.0.0.1." + ); + } + + const port = options.port || 3456; + + // Save the PID of the background process + savePid(process.pid); + + // Handle SIGINT (Ctrl+C) to clean up PID file + process.on("SIGINT", () => { + console.log("Received SIGINT, cleaning up..."); + cleanupPidFile(); + process.exit(0); + }); + + // Handle SIGTERM to clean up PID file + process.on("SIGTERM", () => { + cleanupPidFile(); + process.exit(0); + }); + console.log(HOST) + + // Use port from environment variable if set (for background process) + const servicePort = process.env.SERVICE_PORT + ? parseInt(process.env.SERVICE_PORT) + : port; + const server = createServer({ + jsonPath: CONFIG_FILE, + initialConfig: { + // ...config, + providers: config.Providers || config.providers, + HOST: HOST, + PORT: servicePort, + LOG_FILE: join( + homedir(), + ".claude-code-router", + "claude-code-router.log" + ), + }, + }); + server.addHook("preHandler", apiKeyAuth(config)); + server.addHook("preHandler", async (req, reply) => + router(req, reply, config) + ); + server.start(); +} + +export { run }; +// run(); + + + +================================================ +FILE: src/server.ts +================================================ +import Server from "@musistudio/llms"; + +export const createServer = (config: any): Server => { + const server = new Server(config); + return server; +}; + + + +================================================ +FILE: src/middleware/auth.ts +================================================ +import { FastifyRequest, FastifyReply } from "fastify"; + +export const apiKeyAuth = + (config: any) => + (req: FastifyRequest, reply: FastifyReply, done: () => void) => { + if (["/", "/health"].includes(req.url)) { + return done(); + } + const apiKey = config.APIKEY; + + if (!apiKey) { + return done(); + } + + const authKey: string = + req.headers.authorization || req.headers["x-api-key"]; + if (!authKey) { + reply.status(401).send("APIKEY is missing"); + return; + } + let token = ""; + if (authKey.startsWith("Bearer")) { + token = authKey.split(" ")[1]; + } else { + token = authKey; + } + if (token !== apiKey) { + reply.status(401).send("Invalid API key"); + return; + } + + done(); + }; + + + +================================================ +FILE: src/utils/close.ts +================================================ +import { isServiceRunning, cleanupPidFile, getReferenceCount } from './processCheck'; +import { readFileSync } from 'fs'; +import { HOME_DIR } from '../constants'; +import { join } from 'path'; + +export async function closeService() { + const PID_FILE = join(HOME_DIR, '.claude-code-router.pid'); + + if (!isServiceRunning()) { + console.log("No service is currently running."); + return; + } + + if (getReferenceCount() > 0) { + return; + } + + try { + const pid = parseInt(readFileSync(PID_FILE, 'utf-8')); + process.kill(pid); + cleanupPidFile(); + console.log("claude code router service has been successfully stopped."); + } catch (e) { + console.log("Failed to stop the service. It may have already been stopped."); + cleanupPidFile(); + } +} + + + +================================================ +FILE: src/utils/codeCommand.ts +================================================ +import { spawn } from "child_process"; +import { + incrementReferenceCount, + decrementReferenceCount, +} from "./processCheck"; +import { closeService } from "./close"; +import { readConfigFile } from "."; + +export async function executeCodeCommand(args: string[] = []) { + // Set environment variables + const config = await readConfigFile(); + const env = { + ...process.env, + ANTHROPIC_AUTH_TOKEN: "test", + ANTHROPIC_BASE_URL: `http://127.0.0.1:3456`, + API_TIMEOUT_MS: "600000", + }; + + if (config?.APIKEY) { + env.ANTHROPIC_API_KEY = config.APIKEY; + delete env.ANTHROPIC_AUTH_TOKEN; + } + + // Increment reference count when command starts + incrementReferenceCount(); + + // Execute claude command + const claudePath = process.env.CLAUDE_PATH || "claude"; + const claudeProcess = spawn(claudePath, args, { + env, + stdio: "inherit", + shell: true, + }); + + claudeProcess.on("error", (error) => { + console.error("Failed to start claude command:", error.message); + console.log( + "Make sure Claude Code is installed: npm install -g @anthropic-ai/claude-code" + ); + decrementReferenceCount(); + process.exit(1); + }); + + claudeProcess.on("close", (code) => { + decrementReferenceCount(); + closeService(); + process.exit(code || 0); + }); +} + + + +================================================ +FILE: src/utils/index.ts +================================================ +import fs from "node:fs/promises"; +import readline from "node:readline"; +import { + CONFIG_FILE, + DEFAULT_CONFIG, + HOME_DIR, + PLUGINS_DIR, +} from "../constants"; + +const ensureDir = async (dir_path: string) => { + try { + await fs.access(dir_path); + } catch { + await fs.mkdir(dir_path, { recursive: true }); + } +}; + +export const initDir = async () => { + await ensureDir(HOME_DIR); + await ensureDir(PLUGINS_DIR); +}; + +const createReadline = () => { + return readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); +}; + +const question = (query: string): Promise => { + return new Promise((resolve) => { + const rl = createReadline(); + rl.question(query, (answer) => { + rl.close(); + resolve(answer); + }); + }); +}; + +const confirm = async (query: string): Promise => { + const answer = await question(query); + return answer.toLowerCase() !== "n"; +}; + +export const readConfigFile = async () => { + try { + const config = await fs.readFile(CONFIG_FILE, "utf-8"); + return JSON.parse(config); + } catch { + const name = await question("Enter Provider Name: "); + const APIKEY = await question("Enter Provider API KEY: "); + const baseUrl = await question("Enter Provider URL: "); + const model = await question("Enter MODEL Name: "); + const config = Object.assign({}, DEFAULT_CONFIG, { + Providers: [ + { + name, + api_base_url: baseUrl, + api_key: APIKEY, + models: [model], + }, + ], + Router: { + default: `${name},${model}`, + }, + }); + await writeConfigFile(config); + return config; + } +}; + +export const writeConfigFile = async (config: any) => { + await ensureDir(HOME_DIR); + await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2)); +}; + +export const initConfig = async () => { + const config = await readConfigFile(); + Object.assign(process.env, config); + return config; +}; + + + +================================================ +FILE: src/utils/log.ts +================================================ +import fs from "node:fs"; +import path from "node:path"; +import { HOME_DIR } from "../constants"; + +const LOG_FILE = path.join(HOME_DIR, "claude-code-router.log"); + +// Ensure log directory exists +if (!fs.existsSync(HOME_DIR)) { + fs.mkdirSync(HOME_DIR, { recursive: true }); +} + +export function log(...args: any[]) { + // Check if logging is enabled via environment variable + const isLogEnabled = process.env.LOG === "true"; + + if (!isLogEnabled) { + return; + } + + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${ + Array.isArray(args) + ? args + .map((arg) => + typeof arg === "object" ? JSON.stringify(arg) : String(arg) + ) + .join(" ") + : "" + }\n`; + + // Append to log file + fs.appendFileSync(LOG_FILE, logMessage, "utf8"); +} + + + +================================================ +FILE: src/utils/processCheck.ts +================================================ +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { PID_FILE, REFERENCE_COUNT_FILE } from '../constants'; + +export function incrementReferenceCount() { + let count = 0; + if (existsSync(REFERENCE_COUNT_FILE)) { + count = parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0; + } + count++; + writeFileSync(REFERENCE_COUNT_FILE, count.toString()); +} + +export function decrementReferenceCount() { + let count = 0; + if (existsSync(REFERENCE_COUNT_FILE)) { + count = parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0; + } + count = Math.max(0, count - 1); + writeFileSync(REFERENCE_COUNT_FILE, count.toString()); +} + +export function getReferenceCount(): number { + if (!existsSync(REFERENCE_COUNT_FILE)) { + return 0; + } + return parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0; +} + +export function isServiceRunning(): boolean { + if (!existsSync(PID_FILE)) { + return false; + } + + try { + const pid = parseInt(readFileSync(PID_FILE, 'utf-8')); + process.kill(pid, 0); + return true; + } catch (e) { + // Process not running, clean up pid file + cleanupPidFile(); + return false; + } +} + +export function savePid(pid: number) { + writeFileSync(PID_FILE, pid.toString()); +} + +export function cleanupPidFile() { + if (existsSync(PID_FILE)) { + try { + const fs = require('fs'); + fs.unlinkSync(PID_FILE); + } catch (e) { + // Ignore cleanup errors + } + } +} + +export function getServicePid(): number | null { + if (!existsSync(PID_FILE)) { + return null; + } + + try { + const pid = parseInt(readFileSync(PID_FILE, 'utf-8')); + return isNaN(pid) ? null : pid; + } catch (e) { + return null; + } +} + +export function getServiceInfo() { + const pid = getServicePid(); + const running = isServiceRunning(); + + return { + running, + pid, + port: 3456, + endpoint: 'http://127.0.0.1:3456', + pidFile: PID_FILE, + referenceCount: getReferenceCount() + }; +} + + + +================================================ +FILE: src/utils/router.ts +================================================ +import { MessageCreateParamsBase } from "@anthropic-ai/sdk/resources/messages"; +import { get_encoding } from "tiktoken"; +import { log } from "./log"; + +const enc = get_encoding("cl100k_base"); + +const getUseModel = (req: any, tokenCount: number, config: any) => { + if (req.body.model.includes(",")) { + return req.body.model; + } + // if tokenCount is greater than 60K, use the long context model + if (tokenCount > 1000 * 60 && config.Router.longContext) { + log("Using long context model due to token count:", tokenCount); + return config.Router.longContext; + } + // If the model is claude-3-5-haiku, use the background model + if (req.body.model?.startsWith("claude-3-5-haiku") && config.Router.background) { + log("Using background model for ", req.body.model); + return config.Router.background; + } + // if exits thinking, use the think model + if (req.body.thinking && config.Router.think) { + log("Using think model for ", req.body.thinking); + return config.Router.think; + } + return config.Router!.default; +}; + +export const router = async (req: any, res: any, config: any) => { + const { messages, system = [], tools }: MessageCreateParamsBase = req.body; + try { + let tokenCount = 0; + if (Array.isArray(messages)) { + messages.forEach((message) => { + if (typeof message.content === "string") { + tokenCount += enc.encode(message.content).length; + } else if (Array.isArray(message.content)) { + message.content.forEach((contentPart) => { + if (contentPart.type === "text") { + tokenCount += enc.encode(contentPart.text).length; + } else if (contentPart.type === "tool_use") { + tokenCount += enc.encode( + JSON.stringify(contentPart.input) + ).length; + } else if (contentPart.type === "tool_result") { + tokenCount += enc.encode( + typeof contentPart.content === "string" + ? contentPart.content + : JSON.stringify(contentPart.content) + ).length; + } + }); + } + }); + } + if (typeof system === "string") { + tokenCount += enc.encode(system).length; + } else if (Array.isArray(system)) { + system.forEach((item) => { + if (item.type !== "text") return; + if (typeof item.text === "string") { + tokenCount += enc.encode(item.text).length; + } else if (Array.isArray(item.text)) { + item.text.forEach((textPart) => { + tokenCount += enc.encode(textPart || "").length; + }); + } + }); + } + if (tools) { + tools.forEach((tool) => { + if (tool.description) { + tokenCount += enc.encode(tool.name + tool.description).length; + } + if (tool.input_schema) { + tokenCount += enc.encode(JSON.stringify(tool.input_schema)).length; + } + }); + } + const model = getUseModel(req, tokenCount, config); + req.body.model = model; + } catch (error: any) { + log("Error in router middleware:", error.message); + req.body.model = config.Router!.default; + } + return; +}; + + + +================================================ +FILE: src/utils/status.ts +================================================ +import { getServiceInfo } from './processCheck'; + +export function showStatus() { + const info = getServiceInfo(); + + console.log('\n📊 Claude Code Router Status'); + console.log('═'.repeat(40)); + + if (info.running) { + console.log('✅ Status: Running'); + console.log(`🆔 Process ID: ${info.pid}`); + console.log(`🌐 Port: ${info.port}`); + console.log(`📡 API Endpoint: ${info.endpoint}`); + console.log(`📄 PID File: ${info.pidFile}`); + console.log(''); + console.log('🚀 Ready to use! Run the following commands:'); + console.log(' ccr code # Start coding with Claude'); + console.log(' ccr stop # Stop the service'); + } else { + console.log('❌ Status: Not Running'); + console.log(''); + console.log('💡 To start the service:'); + console.log(' ccr start'); + } + + console.log(''); +} + diff --git a/Claudia-docs/Overview Claudia.md b/Claudia-docs/Overview Claudia.md new file mode 100644 index 00000000..4eaf0b40 --- /dev/null +++ b/Claudia-docs/Overview Claudia.md @@ -0,0 +1,67 @@ +# Tutorial: claudia + +Claudia is a **desktop application** designed to enhance the *developer experience* with **Claude Code**, an AI-powered coding assistant. It provides an intuitive **graphical user interface** to manage coding projects, interact with Claude through intelligent "chat" sessions, define and run **custom AI agents** for automated tasks, and use a unique **checkpointing system** for "time-travel" through code changes and conversation history. The application integrates tightly with the underlying Claude Code CLI, offering advanced features like real-time output streaming, configurable hooks, and persistent session management, all within a familiar **multi-tabbed environment**. + + +## Visual Overview + +```mermaid +flowchart TD + A0["Tauri Commands (API Layer) +"] + A1["Claude Code Session +"] + A2["CC Agents System +"] + A3["Checkpointing System +"] + A4["Hooks Configuration +"] + A5["Process Management (Registry) +"] + A6["Tab Management +"] + A0 -- "Executes sessions" --> A1 + A0 -- "Manages agents" --> A2 + A0 -- "Performs operations" --> A3 + A0 -- "Configures settings" --> A4 + A0 -- "Registers processes" --> A5 + A1 -- "Calls API" --> A0 + A1 -- "Uses history" --> A3 + A1 -- "Applies custom logic" --> A4 + A1 -- "Renders as tab" --> A6 + A2 -- "Calls API" --> A0 + A2 -- "Applies agent rules" --> A4 + A2 -- "Tracks runs" --> A5 + A2 -- "Renders as tab" --> A6 + A3 -- "Provides history" --> A1 + A4 -- "Defines behavior" --> A1 + A4 -- "Defines behavior" --> A2 + A5 -- "Monitors activity" --> A1 + A5 -- "Monitors activity" --> A2 + A6 -- "Triggers actions" --> A0 + A6 -- "Hosts UI" --> A1 + A6 -- "Hosts UI" --> A2 +``` + +## Chapters + +1. [Tab Management +](01_tab_management_.md) +2. [Claude Code Session +](02_claude_code_session_.md) +3. [CC Agents System +](03_cc_agents_system_.md) +4. [Checkpointing System +](04_checkpointing_system_.md) +5. [Hooks Configuration +](05_hooks_configuration_.md) +6. [Tauri Commands (API Layer) +](06_tauri_commands__api_layer__.md) +7. [Process Management (Registry) +](07_process_management__registry__.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/Overview Claudia.md b/Claudia-docs/V_1/Overview Claudia.md new file mode 100644 index 00000000..0648d9e3 --- /dev/null +++ b/Claudia-docs/V_1/Overview Claudia.md @@ -0,0 +1,74 @@ +# Tutorial: claudia + +Claudia is a desktop application that **interacts** with the `claude` command-line interface (CLI) to help you manage AI-assisted projects. It allows you to create custom **Agents** with specific instructions and security **Sandboxing** rules, browse your project **Sessions**, track changes with **Checkpointing**, and connect to external AI model servers using **MCP**. The app provides a user-friendly **Frontend UI** built using **Tauri Commands** to bridge the frontend and backend logic, processing **Streamed Output** from Claude and managing running tasks via a **Process Registry**. + + +## Visual Overview + +```mermaid +flowchart TD + A0["Tauri Commands +"] + A1["Claude CLI Interaction +"] + A2["Agents +"] + A3["Sandboxing +"] + A4["Checkpointing +"] + A5["MCP (Model Context Protocol) +"] + A6["Frontend UI Components +"] + A7["Session/Project Management +"] + A8["Streamed Output Processing +"] + A9["Process Registry +"] + A6 -- "Invokes commands" --> A0 + A0 -- "Provides data" --> A6 + A0 -- "Controls execution" --> A1 + A0 -- "Manages" --> A2 + A0 -- "Manages config" --> A3 + A0 -- "Manages" --> A4 + A0 -- "Manages servers" --> A5 + A0 -- "Lists data" --> A7 + A0 -- "Manages processes" --> A9 + A2 -- "Uses for execution" --> A1 + A2 -- "Configures permissions" --> A3 + A2 -- "Registers runs" --> A9 + A1 -- "Produces stream" --> A8 + A3 -- "Enforces rules on" --> A1 + A8 -- "Displays output" --> A6 + A4 -- "Version control for" --> A7 + A9 -- "Stores live data" --> A8 +``` + +## Chapters + +1. [Session/Project Management +](01_session_project_management_.md) +2. [Agents +](02_agents_.md) +3. [Frontend UI Components +](03_frontend_ui_components_.md) +4. [Tauri Commands +](04_tauri_commands_.md) +5. [Claude CLI Interaction +](05_claude_cli_interaction_.md) +6. [Sandboxing +](06_sandboxing_.md) +7. [Streamed Output Processing +](07_streamed_output_processing_.md) +8. [Process Registry +](08_process_registry_.md) +9. [Checkpointing +](09_checkpointing_.md) +10. [MCP (Model Context Protocol) +](10_mcp__model_context_protocol__.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). diff --git a/Claudia-docs/V_1/claudia-1.Session-Project Management.md b/Claudia-docs/V_1/claudia-1.Session-Project Management.md new file mode 100644 index 00000000..b1a50b8c --- /dev/null +++ b/Claudia-docs/V_1/claudia-1.Session-Project Management.md @@ -0,0 +1,429 @@ + +# Chapter 1: Session/Project Management + +Welcome to the first chapter of the `claudia` tutorial! In this chapter, we'll explore how `claudia` helps you keep track of your work with Claude Code using its Session/Project Management feature. + +Imagine you're using Claude Code to help you build a new feature in a software project. You spend hours talking to Claude, asking it to write code, explain concepts, and debug issues. This interaction is a "session". Your feature development is happening within a specific folder on your computer – that's your "project". + +As you work on different projects, you'll have many sessions. How do you find that helpful conversation you had last week about a bug fix in your "website-redesign" project? How do you pick up where you left off? This is exactly what the Session/Project Management part of `claudia` solves! + +It's like having a digital filing cabinet for all your Claude Code conversations, organized by the project you were working on. + +## What are Projects and Sessions in `claudia`? + +At its core, Session/Project Management deals with two main ideas: + +1. **Projects:** A "Project" in `claudia` (and the underlying Claude Code CLI) corresponds to a directory on your computer where you were running Claude Code. When you start Claude Code in a folder, it recognizes that as a project. +2. **Sessions:** A "Session" is a single, continuous conversation you had with Claude Code within a specific project. Every time you run the `claude` command (or `claude --resume`, `claude --continue`), you're starting or continuing a session. + +The Claude Code CLI automatically saves your conversation history. `claudia` reads this saved history to show you what you've done. + +## Where is the Data Stored? + +The Claude Code CLI stores everything it needs inside a special directory in your home folder: `~/.claude`. + +Inside `~/.claude`, you'll find: + +* A `projects` directory: This is where information about your projects and their sessions is kept. +* Other files like `settings.json` or `CLAUDE.md` (we'll talk about settings and `CLAUDE.md` later). + +Each project you've worked on will have a subdirectory inside `~/.claude/projects`. The name of this subdirectory is a special encoded version of the project's file path. + +Inside a project's directory (`~/.claude/projects/your-project-id/`), you'll find files ending in `.jsonl`. Each `.jsonl` file is a single **session**. The name of the file (before `.jsonl`) is the unique ID for that session. These files contain a history of messages, commands, and tool outputs for that specific conversation. + +## How Does `claudia` Show You Your History? + +Let's look at the user interface of `claudia`. When you open it, you'll likely see a list of your recent projects. Clicking on a project takes you to a list of sessions within that project. You can then click on a session to view its history or resume it. + +Here's a simplified look at how the frontend components display this information: + +```typescript +// src/components/ProjectList.tsx - Simplified structure +import { Card, CardContent } from "@/components/ui/card"; +import type { Project } from "@/lib/api"; + +interface ProjectListProps { + projects: Project[]; // This array comes from the backend + onProjectClick: (project: Project) => void; +} + +export const ProjectList: React.FC = ({ projects, onProjectClick }) => { + return ( +
+ {/* Loop through the projects array */} + {projects.map((project) => ( + onProjectClick(project)}> + +
+

{project.path}

{/* Display the project path */} +

{project.sessions.length} sessions

{/* Show session count */} + {/* ... other project info like creation date */} +
+ {/* ... click handler */} +
+
+ ))} + {/* ... Pagination */} +
+ ); +}; +``` + +This component (`ProjectList.tsx`) takes a list of `Project` objects (fetched from the backend) and renders a card for each one, showing basic info like the project path and how many sessions it contains. When you click a card, it calls the `onProjectClick` function, typically navigating you to the sessions list for that project. + +Next, let's look at how the sessions for a selected project are displayed: + +```typescript +// src/components/SessionList.tsx - Simplified structure +import { Card, CardContent } from "@/components/ui/card"; +import type { Session } from "@/lib/api"; + +interface SessionListProps { + sessions: Session[]; // This array comes from the backend for the selected project + projectPath: string; + onSessionClick?: (session: Session) => void; + onBack: () => void; // Button to go back to project list +} + +export const SessionList: React.FC = ({ sessions, projectPath, onSessionClick, onBack }) => { + return ( +
+ {/* Back button */} +

{projectPath}

{/* Display the current project path */} +
+ {/* Loop through the sessions array */} + {sessions.map((session) => ( + onSessionClick?.(session)}> + +
+

Session ID: {session.id.slice(0, 8)}...

{/* Display truncated session ID */} + {/* Display the first message preview if available */} + {session.first_message &&

First msg: {session.first_message}

} + {/* ... other session info like timestamps */} +
+ {/* ... click handler */} +
+
+ ))} +
+ {/* ... Pagination */} +
+ ); +}; +``` + +The `SessionList.tsx` component receives the list of sessions for a *single* project (again, fetched from the backend). It shows you the project path you're currently viewing and lists each session, often including its ID, creation time, and a preview of the first message. Clicking a session calls `onSessionClick`, which will lead to the conversation view (`ClaudeCodeSession.tsx`). + +## How it Works: Under the Hood + +The frontend components we just saw need data to display. This data is provided by the backend code, which runs in Rust using the Tauri framework. The backend's job for Session/Project Management is to read the files in the `~/.claude` directory and structure that information for the frontend. + +Here's a simplified step-by-step of what happens when the frontend asks for the list of projects: + +1. The frontend calls a backend command, specifically `list_projects`. +2. The backend code starts by finding the `~/.claude` directory on your computer. +3. It then looks inside the `~/.claude/projects` directory. +4. For each directory found inside `projects`, it treats it as a potential project. +5. It reads the name of the project directory (which is an encoded path) and tries to find the actual project path by looking at the session files inside. +6. It also counts the number of `.jsonl` files (sessions) inside that project directory. +7. It gets the creation timestamp of the project directory. +8. It gathers this information (project ID, path, sessions list, creation time) into a `Project` struct. +9. It repeats this for all project directories. +10. Finally, it sends a list of these `Project` structs back to the frontend. + +Fetching sessions for a specific project follows a similar pattern: + +1. The frontend calls the `get_project_sessions` command, providing the `project_id`. +2. The backend finds the specific project directory inside `~/.claude/projects` using the provided `project_id`. +3. It looks inside that project directory for all `.jsonl` files. +4. For each `.jsonl` file (session), it extracts the session ID from the filename. +5. It gets the file's creation timestamp. +6. It reads the *first few lines* of the `.jsonl` file to find the first user message and its timestamp, for display as a preview in the UI. +7. It might also check for related files like todo data (`.json` files in `~/.claude/todos` linked by session ID). +8. It gathers this info into a `Session` struct. +9. It repeats this for all session files in the project directory. +10. Finally, it sends a list of `Session` structs back to the frontend. + +Here's a sequence diagram illustrating the `list_projects` flow: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Filesystem as ~/.claude + + User->>Frontend: Open Projects View + Frontend->>Backend: Call list_projects() + Backend->>Filesystem: Read ~/.claude/projects directory + Filesystem-->>Backend: List of project directories + Backend->>Filesystem: For each directory: Read contents (session files) + Filesystem-->>Backend: List of session files + Backend->>Backend: Process directories and files (create Project structs) + Backend-->>Frontend: Return List + Frontend->>User: Display Project List +``` + +And the `get_project_sessions` flow: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Filesystem as ~/.claude + + User->>Frontend: Click on a Project + Frontend->>Backend: Call get_project_sessions(projectId) + Backend->>Filesystem: Read ~/.claude/projects/projectId/ directory + Filesystem-->>Backend: List of session files (.jsonl) + Backend->>Filesystem: For each session file: Read first lines, read metadata + Filesystem-->>Backend: First message, timestamp, creation time, etc. + Backend->>Backend: Process session files (create Session structs) + Backend-->>Frontend: Return List + Frontend->>User: Display Session List for Project +``` + +## Diving into the Code + +Let's look at some specific parts of the Rust code in `src-tauri/src/commands/claude.rs` that handle this logic. + +First, the data structures that represent a project and a session: + +```rust +// src-tauri/src/commands/claude.rs +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub id: String, // The encoded directory name + pub path: String, // The decoded or detected real path + pub sessions: Vec, // List of session file names (IDs) + pub created_at: u64, // Timestamp +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + pub id: String, // The session file name (UUID) + pub project_id: String, // Link back to the project + pub project_path: String, // The project's real path + pub todo_data: Option, // Optional associated data + pub created_at: u64, // Timestamp + pub first_message: Option, // Preview of the first user message + pub message_timestamp: Option, // Timestamp of the first message +} +// ... rest of the file +``` + +These `struct` definitions tell us what information the backend collects and sends to the frontend for projects and sessions. Notice the `Serialize` and `Deserialize` derives; this is what allows Tauri to easily pass these structures between the Rust backend and the JavaScript/TypeScript frontend. + +Here's the function that finds the base `~/.claude` directory: + +```rust +// src-tauri/src/commands/claude.rs +fn get_claude_dir() -> Result { + dirs::home_dir() // Find the user's home directory + .context("Could not find home directory")? // Handle potential error + .join(".claude") // Append the .claude directory name + .canonicalize() // Resolve symbolic links, etc. + .context("Could not find ~/.claude directory") // Handle potential error +} +// ... rest of the file +``` + +This simple function is crucial as all project and session data is located relative to `~/.claude`. + +Now, a look at the `list_projects` function. We'll skip some error handling and logging for brevity here: + +```rust +// src-tauri/src/commands/claude.rs +#[tauri::command] +pub async fn list_projects() -> Result, String> { + let claude_dir = get_claude_dir().map_err(|e| e.to_string())?; + let projects_dir = claude_dir.join("projects"); // Path to ~/.claude/projects + + if !projects_dir.exists() { + return Ok(Vec::new()); // Return empty list if directory doesn't exist + } + + let mut projects = Vec::new(); + + // Iterate over entries inside ~/.claude/projects + let entries = fs::read_dir(&projects_dir).map_err(|e| format!("..."))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("..."))?; + let path = entry.path(); + + if path.is_dir() { // Only process directories + let dir_name = path.file_name().and_then(|n| n.to_str()).ok_or_else(|| "...").unwrap(); + + // Get creation/modification timestamp + let metadata = fs::metadata(&path).map_err(|e| format!("..."))?; + let created_at = metadata.created().or_else(|_| metadata.modified()).unwrap_or(SystemTime::UNIX_EPOCH).duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); + + // Determine the actual project path (explained next) + let project_path = match get_project_path_from_sessions(&path) { + Ok(p) => p, + Err(_) => decode_project_path(dir_name) // Fallback if session files don't exist + }; + + // Find all session files (.jsonl) in this project directory + let mut sessions = Vec::new(); + if let Ok(session_entries) = fs::read_dir(&path) { + for session_entry in session_entries.flatten() { + let session_path = session_entry.path(); + if session_path.is_file() && session_path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + if let Some(session_id) = session_path.file_stem().and_then(|s| s.to_str()) { + sessions.push(session_id.to_string()); // Store session ID (filename) + } + } + } + } + + // Add the project to the list + projects.push(Project { + id: dir_name.to_string(), + path: project_path, + sessions, + created_at, + }); + } + } + + projects.sort_by(|a, b| b.created_at.cmp(&a.created_at)); // Sort newest first + Ok(projects) +} +// ... rest of the file +``` + +This code reads the `projects` directory, identifies subdirectories as projects, and collects basic information for each. A key part is determining the *actual* project path, as the directory name is an encoded version of the path where Claude Code was run. The `get_project_path_from_sessions` function handles this: + +```rust +// src-tauri/src/commands/claude.rs +fn get_project_path_from_sessions(project_dir: &PathBuf) -> Result { + // Try to read any JSONL file in the directory + let entries = fs::read_dir(project_dir) + .map_err(|e| format!("..."))?; + + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + // Read the first line of the JSONL file + if let Ok(file) = fs::File::open(&path) { + let reader = BufReader::new(file); + if let Some(Ok(first_line)) = reader.lines().next() { + // Parse the JSON and extract "cwd" (current working directory) + if let Ok(json) = serde_json::from_str::(&first_line) { + if let Some(cwd) = json.get("cwd").and_then(|v| v.as_str()) { + return Ok(cwd.to_string()); // Found the project path! + } + } + } + } + } + } + } + + Err("Could not determine project path from session files".to_string()) // Failed to find it +} +// ... rest of the file +``` + +This function is smarter than just decoding the directory name. It opens the first session file it finds within a project directory, reads the very first line (which usually contains metadata including the `cwd` - current working directory - where Claude Code was launched), and uses that `cwd` as the definitive project path. This is more reliable than trying to decode the directory name. + +Finally, let's look at `get_project_sessions`: + +```rust +// src-tauri/src/commands/claude.rs +#[tauri::command] +pub async fn get_project_sessions(project_id: String) -> Result, String> { + let claude_dir = get_claude_dir().map_err(|e| e.to_string())?; + let project_dir = claude_dir.join("projects").join(&project_id); // Path to specific project dir + let todos_dir = claude_dir.join("todos"); // Path to todo data + + if !project_dir.exists() { + return Err(format!("Project directory not found: {}", project_id)); + } + + // Determine the actual project path + let project_path = match get_project_path_from_sessions(&project_dir) { + Ok(p) => p, + Err(_) => decode_project_path(&project_id) // Fallback + }; + + let mut sessions = Vec::new(); + + // Read all files in the project directory + let entries = fs::read_dir(&project_dir).map_err(|e| format!("..."))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("..."))?; + let path = entry.path(); + + // Process only .jsonl files + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + if let Some(session_id) = path.file_stem().and_then(|s| s.to_str()) { // Get filename as session ID + // Get file creation timestamp + let metadata = fs::metadata(&path).map_err(|e| format!("..."))?; + let created_at = metadata.created().or_else(|_| metadata.modified()).unwrap_or(SystemTime::UNIX_EPOCH).duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); + + // Extract first user message for preview (explained next) + let (first_message, message_timestamp) = extract_first_user_message(&path); + + // Check for associated todo data file + let todo_path = todos_dir.join(format!("{}.json", session_id)); + let todo_data = if todo_path.exists() { + // ... read and parse todo.json ... + None // Simplified: just show if it exists, not the data + } else { + None + }; + + // Add the session to the list + sessions.push(Session { + id: session_id.to_string(), + project_id: project_id.clone(), + project_path: project_path.clone(), + todo_data, + created_at, + first_message, + message_timestamp, + }); + } + } + } + + sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); // Sort newest first + Ok(sessions) +} +// ... rest of the file +``` + +This function is similar to `list_projects` but focuses on one project directory. It iterates through the files, identifies the `.jsonl` session files, extracts metadata like ID and timestamp, and importantly, calls `extract_first_user_message` to get a quick preview of the conversation's start for the UI. + +The `extract_first_user_message` function reads the session's `.jsonl` file line by line, parses each line as JSON, and looks for the first entry that represents a message from the "user" role, making sure to skip certain types of messages (like the initial system caveat or command outputs) to find the actual user prompt. + +## Putting it Together + +So, the Session/Project Management feature in `claudia` works by: + +1. Reading the file structure created by the Claude Code CLI in `~/.claude`. +2. Identifying directories in `~/.claude/projects` as projects and `.jsonl` files within them as sessions. +3. Extracting key metadata (IDs, paths, timestamps, first message previews). +4. Providing this structured data to the frontend UI via Tauri commands (`list_projects`, `get_project_sessions`). +5. Allowing the frontend (`ProjectList.tsx`, `SessionList.tsx`) to display this information in an organized, browsable way. +6. Enabling the user to select a session, triggering navigation to the main session view (`ClaudeCodeSession.tsx`) where they can see the full history (loaded using `load_session_history`) and potentially resume the conversation. + +This abstraction provides the essential foundation for interacting with your past Claude Code work, allowing you to manage your conversation history effectively. + +## Conclusion + +In this chapter, we learned how `claudia` discovers, lists, and displays your Claude Code projects and sessions by reading files from the `~/.claude` directory. We saw how the frontend components like `ProjectList` and `SessionList` use data provided by backend commands like `list_projects` and `get_project_sessions` to build the navigation interface. We also briefly touched upon how session data (`.jsonl` files) is parsed to show previews. + +Understanding how `claudia` manages sessions and projects is the first step in seeing how it builds a rich user interface on top of the command-line tool. In the next chapter, we'll dive into the concept of [Agents](02_agents_.md), which are central to how Claude Code and `claudia` understand the context of your work. + +[Next Chapter: Agents](02_agents_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/claude.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ProjectList.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/SessionList.tsx) diff --git a/Claudia-docs/V_1/claudia-10.MCP.md b/Claudia-docs/V_1/claudia-10.MCP.md new file mode 100644 index 00000000..cc0e754f --- /dev/null +++ b/Claudia-docs/V_1/claudia-10.MCP.md @@ -0,0 +1,505 @@ +# Chapter 10: MCP (Model Context Protocol) + +Welcome to the final chapter of the `claudia` tutorial! We've covered a lot, from managing your work with [Session/Project Management](01_session_project_management_.md) and defining specialized [Agents](02_agents_.md), to understanding how the [Frontend UI Components](03_frontend_ui_components_.md) are built and how they talk to the backend using [Tauri Commands](04_tauri_commands_.md). We've seen how `claudia` interacts with the core [Claude CLI Interaction](05_claude_cli_interaction_.md), how [Sandboxing](06_sandboxing_.md) keeps your environment secure, how [Streamed Output Processing](07_streamed_output_processing_.md) provides real-time feedback, and how the [Process Registry](08_process_registry_.md) tracks running tasks. Finally, we explored [Checkpointing](09_checkpointing_.md) for versioning your sessions. + +Now, let's look at a feature that allows `claudia` (specifically, the `claude` CLI it controls) to go beyond just interacting with Anthropic's standard Claude API: **MCP (Model Context Protocol)**. + +## The Problem: Connecting to Different AI Sources + +By default, the `claude` CLI is primarily designed to connect to Anthropic's Claude API endpoints (like the ones that power Sonnet, Opus, etc.). But what if you want to use a different AI model? Perhaps a smaller model running locally on your machine, a specialized AI tool you built, or an internal AI service within your company? + +These other AI sources might have different ways of communicating. You need a standard way for `claudia` (or rather, the `claude` CLI it manages) to talk to *any* AI service that can process prompts, use tools, and respond, regardless of who built it or how it runs. + +This is the problem MCP solves. It provides a standardized "language" or "interface" that allows `claude` to communicate with any external program or service that "speaks" MCP. + +Imagine `claudia` is a smart home hub. It needs to talk to various devices – lights, thermostats, speakers – made by different companies. Instead of needing a unique connection method for every single brand, they all agree to use a standard protocol (like Wi-Fi and a common API). MCP is that standard protocol for AI model servers. + +## What is MCP (Model Context Protocol)? + +MCP stands for **Model Context Protocol**. It's a standard protocol used by the `claude` CLI to exchange information with external programs or services that act as AI models or tools. + +When you configure an "MCP Server" in `claude` (and thus in `claudia`), you're telling `claude` about an external AI source that it can connect to using the MCP standard. + +This abstraction layer manages: + +1. **Defining Servers:** Telling `claude` about external MCP sources by giving them a name and specifying how to connect (e.g., run a specific command, connect to a URL). +2. **Listing Servers:** Seeing which MCP servers are configured. +3. **Interacting:** When a session or Agent is configured to use a specific MCP server, the `claude` CLI connects to that server (instead of the default Anthropic API) and uses the MCP to send prompts and receive responses. + +This capability extends `claudia`'s potential far beyond just Anthropic's hosted models, enabling connections to a variety of AI models or services that implement the MCP standard. + +## Key Concepts + +Here are the main ideas behind MCP in `claudia` (and `claude`): + +| Concept | Description | Analogy | +| :---------------- | :----------------------------------------------------------------------------------------------------------- | :------------------------------------------------------ | +| **MCP Server** | An external program or service that speaks the MCP standard and can act as an AI model or provide tools. | A smart device (light, speaker) in a smart home system. | +| **Transport** | How `claude` connects to the MCP Server. Common types are `stdio` (running the server as a command-line process) or `sse` (connecting to a network URL via Server-Sent Events). | How the hub talks to the device (e.g., Wi-Fi, Bluetooth). | +| **Scope** | Where the MCP server configuration is stored. Affects who can see/use it: `user` (all projects), `project` (via `.mcp.json` in the project directory), `local` (only this `claudia` instance's settings, usually linked to a project). | Where you save the device setup (e.g., globally in the app, specific to one room setup). | +| **MCP Configuration** | The details needed to connect to a server: name, transport type, command/URL, environment variables, scope. | The device's settings (name, type, how to connect, what room it's in). | + +## Using MCP in the UI + +`claudia` provides a dedicated section to manage MCP servers. You'll typically find this under "Settings" or a similar menu item. + +The `MCPManager.tsx` component is the main view for this: + +```typescript +// src/components/MCPManager.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Card } from "@/components/ui/card"; +// ... other imports like api, MCPServerList, MCPAddServer, MCPImportExport ... + +export const MCPManager: React.FC = ({ onBack, className }) => { + const [activeTab, setActiveTab] = useState("servers"); // State for the active tab + const [servers, setServers] = useState([]); // State for the list of servers + const [loading, setLoading] = useState(true); + // ... error/toast state ... + + // Load servers when the component mounts + useEffect(() => { + loadServers(); + }, []); + + // Function to load servers from the backend + const loadServers = async () => { + try { + setLoading(true); + // Call the backend command to list servers + const serverList = await api.mcpList(); + setServers(serverList); // Update state + } catch (err) { + console.error("Failed to load MCP servers:", err); + // ... set error state ... + } finally { + setLoading(false); + } + }; + + // Callbacks for child components (Add, List, Import) + const handleServerAdded = () => { + loadServers(); // Refresh the list after adding + setActiveTab("servers"); // Switch back to the list view + // ... show success toast ... + }; + + const handleServerRemoved = (name: string) => { + setServers(prev => prev.filter(s => s.name !== name)); // Remove server from state + // ... show success toast ... + }; + + const handleImportCompleted = (imported: number, failed: number) => { + loadServers(); // Refresh after import + // ... show import result toast ... + }; + + return ( +
{/* Layout container */} + {/* Header with Back button */} +
+ +

MCP Servers

+
+ + {/* Tabs for navigating sections */} + + + Servers + Add Server + Import/Export + + + {/* Server List Tab Content */} + + {/* Using a Card component */} + + + + + {/* Add Server Tab Content */} + + {/* Using a Card component */} + + + + + {/* Import/Export Tab Content */} + + {/* Using a Card component */} + + + + + + {/* ... Toast notifications ... */} +
+ ); +}; +``` + +This main component uses tabs to organize the different MCP management tasks: +* **Servers:** Shows a list of configured servers using the `MCPServerList` component. +* **Add Server:** Provides a form to manually add a new server using the `MCPAddServer` component. +* **Import/Export:** Contains options to import servers (e.g., from a JSON file or Claude Desktop config) or potentially export them, using the `MCPImportExport` component. + +The `MCPServerList.tsx` component simply takes the list of `MCPServer` objects and displays them, grouped by scope (User, Project, Local). It provides buttons to remove or test the connection for each server, calling the relevant `onServerRemoved` or backend test command. + +The `MCPAddServer.tsx` component presents a form where you can enter the details of a new server: name, select the transport type (Stdio or SSE), provide the command or URL, add environment variables, and choose the scope. When you click "Add", it calls the backend `api.mcpAdd` command. + +```typescript +// src/components/MCPAddServer.tsx (Simplified) +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { SelectComponent } from "@/components/ui/select"; +// ... other imports like api ... + +export const MCPAddServer: React.FC = ({ onServerAdded, onError }) => { + const [transport, setTransport] = useState<"stdio" | "sse">("stdio"); + const [serverName, setServerName] = useState(""); + const [commandOrUrl, setCommandOrUrl] = useState(""); + const [scope, setScope] = useState("local"); + // ... state for args, env vars, saving indicator ... + + const handleAddServer = async () => { + if (!serverName.trim() || !commandOrUrl.trim()) { + onError("Name and Command/URL are required"); + return; + } + + try { + // ... set saving state ... + + // Prepare arguments based on transport type + const command = transport === "stdio" ? commandOrUrl : undefined; + const url = transport === "sse" ? commandOrUrl : undefined; + const args = transport === "stdio" ? commandOrUrl.split(/\s+/).slice(1) : []; // Simplified arg parsing + const env = {}; // Simplified env vars + + // Call the backend API command + const result = await api.mcpAdd( + serverName, + transport, + command, + args, + env, + url, + scope + ); + + if (result.success) { + // Reset form and notify parent + setServerName(""); + setCommandOrUrl(""); + setScope("local"); + // ... reset args/env ... + onServerAdded(); + } else { + onError(result.message); // Show error from backend + } + } catch (error) { + onError("Failed to add server"); + console.error("Failed to add MCP server:", error); + } finally { + // ... unset saving state ... + } + }; + + return ( +
+

Add MCP Server

+ setTransport(v as "stdio" | "sse")}> + + Stdio + SSE + + {/* ... Form fields based on transport type (Name, Command/URL, Scope, Env) ... */} + + +
+ ); +}; +``` + +This component collects user input and passes it to the `api.mcpAdd` function, which is a wrapper around the backend Tauri command. + +Once an MCP server is configured, it can potentially be selected as the "model" for an Agent run or an interactive session, although the integration point for selecting MCP servers specifically during session execution might be evolving or limited in the current `claudia` UI compared to standard Anthropic models. The core mechanism is that the `claude` CLI itself is told *which* configured MCP server to use for a task via command-line arguments, rather than connecting directly to Anthropic. + +## How it Works: Under the Hood (Backend) + +The MCP management in `claudia`'s backend (Rust) doesn't re-implement the MCP standard or manage external processes/connections directly for all servers. Instead, it primarily acts as a wrapper around the **`claude mcp`** subcommand provided by the `claude` CLI itself. + +When you use the MCP management features in `claudia`'s UI: + +1. **Frontend Calls Command:** The frontend calls a Tauri command like `mcp_add`, `mcp_list`, or `mcp_remove` ([Chapter 4: Tauri Commands]). +2. **Backend Calls `claude mcp`:** The backend command receives the request and constructs the appropriate command-line arguments for the `claude mcp` subcommand (e.g., `claude mcp add`, `claude mcp list`, `claude mcp remove`). +3. **Backend Spawns Process:** The backend spawns the `claude` binary as a child process, executing it with the prepared `mcp` arguments ([Chapter 5: Claude CLI Interaction]). +4. **`claude` CLI Handles Logic:** The `claude` CLI process receives the `mcp` command and performs the requested action: + * `claude mcp add`: Parses the provided configuration (name, transport, command/URL, scope) and saves it to its own configuration file (usually `~/.claude/mcp.json` for user/local scope, or writes to `.mcp.json` in the project path for project scope). + * `claude mcp list`: Reads its configuration files and prints the list of configured servers to standard output in a specific text format. + * `claude mcp remove`: Removes the specified server from its configuration files. +5. **Backend Captures Output/Status:** `claudia`'s backend captures the standard output and standard error of the `claude mcp` process ([Chapter 7: Streamed Output Processing], though for simple `mcp` commands it's usually just capturing the final output). +6. **Backend Returns Result:** The backend processes the captured output (e.g., parses the list for `mcp list`, checks for success/failure messages for `mcp add`/`remove`) and returns the result back to the frontend. + +For managing project-scoped servers via `.mcp.json`, the backend also contains specific commands (`mcp_read_project_config`, `mcp_save_project_config`) that read and write the `.mcp.json` file directly using Rust's filesystem functions and JSON parsing. This is an alternative way to manage project-specific MCP configurations that doesn't strictly go through the `claude mcp` CLI commands. + +Here's a sequence diagram showing the flow for adding an MCP server using the `mcp_add` command: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI (MCPAddServer.tsx) + participant Backend as Backend Commands (mcp.rs) + participant OS as Operating System + participant ClaudeCLI as claude binary + + User->>Frontend: Fill form & click "Add Server" + Frontend->>Backend: Call mcp_add(name, transport, command, ...) + Backend->>Backend: Construct arguments for "claude mcp add" + Backend->>OS: Spawn process (claude mcp add ...) + OS-->>ClaudeCLI: Start claude binary + ClaudeCLI->>ClaudeCLI: Parse args, update MCP config file (~/.claude/mcp.json or .mcp.json) + ClaudeCLI-->>OS: Process finishes (exit code 0 on success) + OS-->>Backend: Process status & captured output/error + Backend->>Backend: Check status, parse output for result message + Backend-->>Frontend: Return AddServerResult { success, message } + Frontend->>Frontend: Handle result (show toast, refresh list) + Frontend->>User: User sees confirmation/error +``` + +This diagram shows that for server *management* operations (add, list, remove), `claudia` acts as a GUI frontend to the `claude mcp` command-line interface. + +When a session or Agent is configured to *use* one of these registered MCP servers for its AI interactions, the `claude` binary (launched by `claudia` as described in [Chapter 5: Claude CLI Interaction]) is invoked with arguments telling it *which* server to connect to (e.g., `--model mcp:my-server`). The `claude` binary then uses the configuration it previously saved to establish communication with the specified external MCP server using the correct transport (stdio or sse) and protocol. `claudia`'s role during this phase is primarily launching and monitoring the `claude` process, and streaming its output, as covered in previous chapters. + +## Diving into the Backend Code + +Let's look at some snippets from `src-tauri/src/commands/mcp.rs`. + +The helper function `execute_claude_mcp_command` is central to wrapping the CLI calls: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... +use tauri::AppHandle; +use anyhow::{Context, Result}; +use std::process::Command; +use log::info; + +/// Executes a claude mcp command +fn execute_claude_mcp_command(app_handle: &AppHandle, args: Vec<&str>) -> Result { + info!("Executing claude mcp command with args: {:?}", args); + + // Find the claude binary path (logic from Chapter 5) + let claude_path = super::claude::find_claude_binary(app_handle)?; + + // Create a command with inherited environment (helper from Chapter 5) + let mut cmd = super::claude::create_command_with_env(&claude_path); + + cmd.arg("mcp"); // Add the 'mcp' subcommand + for arg in args { + cmd.arg(arg); // Add specific arguments (add, list, remove, get, serve, test-connection, etc.) + } + + // Run the command and capture output + let output = cmd.output() + .context("Failed to execute claude mcp command")?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) // Return stdout on success + } else { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + Err(anyhow::anyhow!("Command failed: {}", stderr)) // Return stderr as error + } +} +``` + +This function simply prepares and runs the `claude mcp ...` command and handles returning the result or error message based on the exit status. + +Now, let's see how `mcp_add` uses this helper: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... + +/// Adds a new MCP server +#[tauri::command] +pub async fn mcp_add( + app: AppHandle, + name: String, + transport: String, + command: Option, + args: Vec, + env: HashMap, + url: Option, + scope: String, +) -> Result { + info!("Adding MCP server: {} with transport: {}", name, transport); + + let mut cmd_args = vec!["add"]; // The 'add' subcommand argument + + // Add arguments for scope, transport, env, name, command/url + // These match the expected arguments for 'claude mcp add' + cmd_args.push("-s"); + cmd_args.push(&scope); + + if transport == "sse" { + cmd_args.push("--transport"); + cmd_args.push("sse"); + } + + for (key, value) in env.iter() { + cmd_args.push("-e"); + cmd_args.push(&format!("{}={}", key, value)); // Format env vars correctly + } + + cmd_args.push(&name); // The server name + + if transport == "stdio" { + if let Some(cmd_str) = &command { + // Handle commands with spaces/args by adding "--" separator if needed + cmd_args.push("--"); + cmd_args.push(cmd_str); + for arg in &args { + cmd_args.push(arg); + } + } else { /* ... error handling ... */ } + } else if transport == "sse" { + if let Some(url_str) = &url { + cmd_args.push(url_str); // The URL for SSE + } else { /* ... error handling ... */ } + } else { /* ... error handling ... */ } + + // Execute the command using the helper + match execute_claude_mcp_command(&app, cmd_args) { + Ok(output) => { + // Parse the output message from claude mcp add + Ok(AddServerResult { + success: true, + message: output.trim().to_string(), + server_name: Some(name), + }) + } + Err(e) => { + // Handle errors from the command execution + Ok(AddServerResult { + success: false, + message: e.to_string(), + server_name: None, + }) + } + } +} +``` + +This command function demonstrates how it builds the `cmd_args` vector, carefully adding the correct flags and values expected by the `claude mcp add` command. It then passes these arguments to `execute_claude_mcp_command` and formats the result into the `AddServerResult` struct for the frontend. + +The `mcp_list` command is similar, executing `claude mcp list` and then parsing the text output (which can be complex, as noted in the code comments) to build the `Vec` structure returned to the frontend. + +Direct file access for `.mcp.json` (project scope) looks like this: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... +use std::path::PathBuf; +use std::fs; +use serde::{Serialize, Deserialize}; + +// Structs mirroring the .mcp.json structure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPProjectConfig { + #[serde(rename = "mcpServers")] + pub mcp_servers: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPServerConfig { + pub command: String, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub env: HashMap, +} + + +/// Reads .mcp.json from the current project +#[tauri::command] +pub async fn mcp_read_project_config(project_path: String) -> Result { + log::info!("Reading .mcp.json from project: {}", project_path); + + let mcp_json_path = PathBuf::from(&project_path).join(".mcp.json"); + + if !mcp_json_path.exists() { + // Return empty config if file doesn't exist + return Ok(MCPProjectConfig { mcp_servers: HashMap::new() }); + } + + match fs::read_to_string(&mcp_json_path) { // Read the file content + Ok(content) => { + match serde_json::from_str::(&content) { // Parse JSON + Ok(config) => Ok(config), + Err(e) => { + log::error!("Failed to parse .mcp.json: {}", e); + Err(format!("Failed to parse .mcp.json: {}", e)) + } + } + } + Err(e) => { + log::error!("Failed to read .mcp.json: {}", e); + Err(format!("Failed to read .mcp.json: {}", e)) + } + } +} + +/// Saves .mcp.json to the current project +#[tauri::command] +pub async fn mcp_save_project_config( + project_path: String, + config: MCPProjectConfig, +) -> Result { + log::info!("Saving .mcp.json to project: {}", project_path); + + let mcp_json_path = PathBuf::from(&project_path).join(".mcp.json"); + + let json_content = serde_json::to_string_pretty(&config) // Serialize config to JSON + .map_err(|e| format!("Failed to serialize config: {}", e))?; + + fs::write(&mcp_json_path, json_content) // Write to the file + .map_err(|e| format!("Failed to write .mcp.json: {}", e))?; + + Ok("Project MCP configuration saved".to_string()) +} +``` + +These commands directly interact with the `.mcp.json` file in the project directory, allowing the UI to edit project-specific configurations without necessarily going through the `claude mcp` command for every change, although `claude` itself will still read this file when run within that project. + +## Conclusion + +In this final chapter, we explored **MCP (Model Context Protocol)**, the standard that allows the `claude` CLI to communicate with external AI model servers running outside the main Claude API. We learned that `claudia` leverages the `claude mcp` subcommand to manage configurations for these external servers, supporting different transport methods (stdio, sse) and scopes (user, project, local). + +We saw how the `claudia` UI provides dedicated sections to list, add, and import MCP servers, and how these actions map to backend Tauri commands. We then looked under the hood to understand that `claudia`'s backend primarily acts as a wrapper, executing `claude mcp` commands to let the `claude` CLI handle the actual configuration management and, during session execution, the communication with the external MCP servers. `claudia` also provides direct file-based management for project-scoped `.mcp.json` configurations. + +Understanding MCP highlights how `claudia` builds a flexible interface on top of `claude`, enabling connections to a potentially diverse ecosystem of AI tools and models that implement this protocol. This extends `claudia`'s capabilities beyond simply interacting with Anthropic's hosted services. + +This concludes our tutorial on the core concepts behind the `claudia` project. We hope this journey through its various components has provided you with a solid understanding of how this application works! + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/mcp.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPAddServer.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPImportExport.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPManager.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPServerList.tsx) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-2.Agents.md b/Claudia-docs/V_1/claudia-2.Agents.md new file mode 100644 index 00000000..d3b08287 --- /dev/null +++ b/Claudia-docs/V_1/claudia-2.Agents.md @@ -0,0 +1,526 @@ +# Chapter 2: Agents + +Welcome back to the `claudia` tutorial! In [Chapter 1: Session/Project Management](01_session_project_management_.md), we learned how `claudia` helps you keep track of your conversations with Claude Code by organizing them into projects and sessions stored in the `~/.claude` directory. + +Now that you know how to find your past work, let's talk about the next key concept in `claudia`: **Agents**. + +## What is an Agent? + +Imagine you use Claude Code for different kinds of tasks. Sometimes you need it to act as a strict code reviewer, sometimes as a creative brainstorming partner, and other times as a focused debugger. Each task might require Claude to have a different "personality" or set of instructions. + +Instead of typing out the same long system prompt (the initial instructions you give to Claude) every time, `claudia` lets you save these configurations as **Agents**. + +Think of an Agent as a pre-packaged, specialized assistant you create within `claudia`. Each Agent is designed for a specific purpose, with its own instructions and capabilities already defined. + +**In simpler terms:** + +* An Agent is like a saved profile for how you want Claude Code to behave. +* You give it a name (like "Bug Hunter" or "Documentation Writer"). +* You give it an icon to easily spot it. +* You give it a "System Prompt" - this is the set of rules or instructions that tell Claude how to act for this specific Agent. For example, a "Bug Hunter" agent might have a system prompt like, "You are an expert Python debugger. Analyze the provided code snippets for potential bugs, common errors, and suggest fixes." +* You can set what permissions it has (like if it's allowed to read or write files). +* You choose which Claude model it should use (like Sonnet or Opus). + +Once an Agent is created, you can select it, give it a specific task (like "debug the function in `main.py`"), choose a project directory, and hit "Execute". `claudia` then runs the Claude Code CLI using *that Agent's* configuration. + +This is much more efficient than manually setting options every time you use Claude Code for a particular job! + +## Key Parts of an Agent + +Let's break down the core components that make up an Agent in `claudia`. You'll configure these when you create or edit an Agent: + +| Part | Description | Why it's important | +| :-------------- | :-------------------------------------------------------------------------- | :-------------------------------------------------- | +| **Name** | A human-readable label (e.g., "Code Reviewer", "Creative Writer"). | Helps you identify the Agent. | +| **Icon** | A visual symbol (e.g., 🤖, ✨, 🛠️). | Makes it easy to find the right Agent at a glance. | +| **System Prompt** | The core instructions given to Claude at the start of the conversation. | Defines the Agent's role, personality, and rules. | +| **Model** | Which Claude model (e.g., Sonnet, Opus) the Agent should use. | Affects performance, capabilities, and cost. | +| **Permissions** | Controls what the Agent is allowed to do (file read/write, network). | **Crucial for security** when running code or tools. | +| **Default Task**| Optional pre-filled text for the task input field when running the Agent. | Saves time for common tasks with this Agent. | + +## Creating and Managing Agents + +`claudia` provides a friendly user interface for managing your Agents. You'll typically find this in the main menu under something like "CC Agents". + +### The Agents List + +When you go to the Agents section, you'll see a list (or grid) of all the Agents you've created. + +You can see their name, icon, and options to: + +* **Execute:** Run the Agent with a new task. +* **Edit:** Change the Agent's configuration. +* **Delete:** Remove the Agent. +* **Create:** Add a brand new Agent. + +Let's look at a simplified frontend component (`CCAgents.tsx`) that displays this list: + +```typescript +// src/components/CCAgents.tsx (Simplified) +// ... imports ... +export const CCAgents: React.FC = ({ onBack, className }) => { + const [agents, setAgents] = useState([]); + // ... state for loading, view mode, etc. ... + + useEffect(() => { + // Fetch agents from the backend when the component loads + const loadAgents = async () => { + try { + const agentsList = await api.listAgents(); // Call backend API + setAgents(agentsList); + } catch (err) { + console.error("Failed to load agents:", err); + } + }; + loadAgents(); + }, []); + + // ... handleDeleteAgent, handleEditAgent, handleExecuteAgent functions ... + // ... state for pagination ... + + return ( + // ... layout code ... + {/* Agents Grid */} +
+ {/* Loop through the fetched agents */} + {agents.map((agent) => ( + + +
{/* Render agent icon */}
+

{agent.name}

+ {/* ... other agent info ... */} +
+ + {/* Buttons to Execute, Edit, Delete */} + + + + +
+ ))} +
+ // ... pagination and other UI elements ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This simplified code shows how the `CCAgents` component fetches a list of `Agent` objects from the backend using `api.listAgents()` and then displays them in cards, providing buttons for common actions. + +### Creating or Editing an Agent + +Clicking "Create" or "Edit" takes you to a different view (`CreateAgent.tsx`). Here, you'll find a form where you can fill in the details of the Agent: name, choose an icon, write the system prompt, select the model, set permissions, and add an optional default task. + +A snippet from the `CreateAgent.tsx` component: + +```typescript +// src/components/CreateAgent.tsx (Simplified) +// ... imports ... +export const CreateAgent: React.FC = ({ + agent, // If provided, we are editing + onBack, + onAgentCreated, + className, +}) => { + const [name, setName] = useState(agent?.name || ""); + const [systemPrompt, setSystemPrompt] = useState(agent?.system_prompt || ""); + // ... state for icon, model, permissions, etc. ... + + const isEditMode = !!agent; + + const handleSave = async () => { + // ... validation ... + try { + // ... set saving state ... + if (isEditMode && agent.id) { + // Call backend API to update agent + await api.updateAgent(agent.id, name, /* ... other fields ... */ systemPrompt, /* ... */); + } else { + // Call backend API to create new agent + await api.createAgent(name, /* ... other fields ... */ systemPrompt, /* ... */); + } + onAgentCreated(); // Notify parent component + } catch (err) { + console.error("Failed to save agent:", err); + // ... show error ... + } finally { + // ... unset saving state ... + } + }; + + // ... handleBack function with confirmation ... + + return ( + // ... layout code ... +
+ {/* Header with Back and Save buttons */} +
+ +

{isEditMode ? "Edit CC Agent" : "Create CC Agent"}

+ +
+ + {/* Form fields */} +
+ {/* Name Input */} +
+ + setName(e.target.value)} /> +
+ + {/* Icon Picker */} + {/* ... component for selecting icon ... */} + + {/* Model Selection */} + {/* ... buttons/radios for model selection ... */} + + {/* Default Task Input */} + {/* ... input for default task ... */} + + {/* Sandbox Settings (Separate Component) */} + + + {/* System Prompt Editor */} +
+ + {/* ... MDEditor component for system prompt ... */} +
+
+
+ // ... Toast Notification ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This component manages the state for the agent's properties and calls either `api.createAgent` or `api.updateAgent` from the backend API layer when the "Save" button is clicked. + +Notice the inclusion of `AgentSandboxSettings`. This is a smaller component (`AgentSandboxSettings.tsx`) specifically for managing the permission toggles: + +```typescript +// src/components/AgentSandboxSettings.tsx (Simplified) +// ... imports ... +export const AgentSandboxSettings: React.FC = ({ + agent, // Receives the current agent state + onUpdate, // Callback to notify parent of changes + className +}) => { + // ... handleToggle function ... + + return ( + + {/* ... Header with Shield icon ... */} +
+ {/* Master sandbox toggle */} +
+ + handleToggle('sandbox_enabled', checked)} // Update parent state + /> +
+ + {/* Permission toggles - conditional render */} + {agent.sandbox_enabled && ( +
+ {/* File Read Toggle */} +
+ + handleToggle('enable_file_read', checked)} // Update parent state + /> +
+ {/* File Write Toggle */} +
+ + handleToggle('enable_file_write', checked)} // Update parent state + /> +
+ {/* Network Toggle */} +
+ + handleToggle('enable_network', checked)} // Update parent state + /> +
+
+ )} + {/* ... Warning when sandbox disabled ... */} +
+
+ ); +}; +``` + +This component simply displays the current sandbox settings for the agent and provides switches to toggle them. When a switch is toggled, it calls the `onUpdate` prop to inform the parent (`CreateAgent`) component, which manages the overall agent state. + +## Executing an Agent + +Once you have agents created, the main purpose is to *run* them. Selecting an agent from the list and clicking "Execute" (or the Play button) takes you to the Agent Execution view (`AgentExecution.tsx`). + +Here's where you: + +1. Select a **Project Path**: This is the directory where the agent will run and where it can potentially read/write files (subject to its permissions). This ties back to the projects we discussed in [Chapter 1: Session/Project Management](01_session_project_management_.md). +2. Enter the **Task**: This is the specific request you have for the agent *for this particular run*. +3. (Optional) Override the **Model**: Choose a different model (Sonnet/Opus) just for this run if needed. +4. Click **Execute**. + +The `AgentExecution.tsx` component handles this: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +export const AgentExecution: React.FC = ({ + agent, // The agent being executed + onBack, + className, +}) => { + const [projectPath, setProjectPath] = useState(""); + const [task, setTask] = useState(""); + const [model, setModel] = useState(agent.model || "sonnet"); // Default to agent's model + const [isRunning, setIsRunning] = useState(false); + const [messages, setMessages] = useState([]); // Output messages + // ... state for stats, errors, etc. ... + + // ... useEffect for listeners and timers ... + + const handleSelectPath = async () => { + // Use Tauri dialog to select a directory + const selected = await open({ directory: true, multiple: false }); + if (selected) { + setProjectPath(selected as string); + } + }; + + const handleExecute = async () => { + if (!projectPath || !task.trim()) return; // Basic validation + + try { + setIsRunning(true); + setMessages([]); // Clear previous output + // ... reset stats, setup listeners ... + + // Call backend API to execute the agent + await api.executeAgent(agent.id!, projectPath, task, model); + + } catch (err) { + console.error("Failed to execute agent:", err); + // ... show error, update state ... + } + }; + + // ... handleStop, handleBackWithConfirmation functions ... + + return ( + // ... layout code ... +
+ {/* Header with Back button and Agent Name */} +
+ +

{agent.name}

+ {/* ... Running status indicator ... */} +
+ + {/* Configuration Section */} +
+ {/* ... Error display ... */} + {/* Project Path Input with Select Button */} +
+ + setProjectPath(e.target.value)} disabled={isRunning} /> + +
+ {/* Model Selection Buttons */} + {/* ... buttons for Sonnet/Opus selection ... */} + {/* Task Input with Execute/Stop Button */} +
+ + setTask(e.target.value)} disabled={isRunning} /> + +
+
+ + {/* Output Display Section */} +
+ {/* Messages are displayed here, streaming as they arrive */} + {/* ... Rendering messages using StreamMessage component ... */} +
+ + {/* Floating Execution Control Bar */} + {/* ... Component showing elapsed time, tokens, etc. ... */} +
+ // ... Fullscreen Modal ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This component uses the `api.executeAgent` Tauri command to start the agent's run. It also sets up event listeners (`agent-output`, `agent-error`, `agent-complete`) to receive data and status updates from the backend *while* the agent is running. This streaming output is then displayed to the user, which we'll cover in more detail in [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md). + +## How it Works: Under the Hood + +Let's peek behind the curtain to understand how `claudia` handles Agents in the backend (Rust code). + +### Agent Storage + +Unlike projects and sessions which are managed by the Claude Code CLI itself in the filesystem (`~/.claude`), `claudia` stores its Agent definitions in a local SQLite database file, typically located within `claudia`'s application data directory (e.g., `~/.config/claudia/agents.db` on Linux, or similar paths on macOS/Windows). + +The `Agent` struct in the Rust backend corresponds to the data stored for each agent: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Agent { + pub id: Option, // Database ID + pub name: String, + pub icon: String, + pub system_prompt: String, + pub default_task: Option, + pub model: String, // e.g., "sonnet", "opus" + // Permissions managed directly on the agent struct + pub sandbox_enabled: bool, + pub enable_file_read: bool, + pub enable_file_write: bool, + pub enable_network: bool, + pub created_at: String, + pub updated_at: String, +} +// ... rest of the file +``` + +The database initialization (`init_database` function) creates the `agents` table to store this information. Backend functions like `list_agents`, `create_agent`, `update_agent`, and `delete_agent` interact with this SQLite database to perform the requested actions. They simply execute standard SQL commands (SELECT, INSERT, UPDATE, DELETE) to manage the `Agent` records. + +Here's a tiny snippet showing a database interaction (listing agents): + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[tauri::command] +pub async fn list_agents(db: State<'_, AgentDb>) -> Result, String> { + let conn = db.0.lock().map_err(|e| e.to_string())?; // Get database connection + + let mut stmt = conn + .prepare("SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents ORDER BY created_at DESC") + .map_err(|e| e.to_string())?; // Prepare SQL query + + let agents = stmt + .query_map([], |row| { // Map database rows to Agent structs + Ok(Agent { + id: Some(row.get(0)?), + name: row.get(1)?, + // ... map other fields ... + system_prompt: row.get(3)?, + model: row.get::<_, String>(5).unwrap_or_else(|_| "sonnet".to_string()), + sandbox_enabled: row.get::<_, bool>(6).unwrap_or(true), + enable_file_read: row.get::<_, bool>(7).unwrap_or(true), + enable_file_write: row.get::<_, bool>(8).unwrap_or(true), + enable_network: row.get::<_, bool>(9).unwrap_or(false), + created_at: row.get(10)?, + updated_at: row.get(11)?, + }) + }) + .map_err(|e| e.to_string())? + .collect::, _>>() + .map_err(|e| e.to_string())?; + + Ok(agents) // Return the list of Agent structs +} +``` + +This snippet shows how `list_agents` connects to the database, prepares a simple `SELECT` statement, and then uses `query_map` to convert each row returned by the database into an `Agent` struct, which is then sent back to the frontend. + +### Agent Execution Flow + +When you click "Execute" for an Agent: + +1. The frontend (`AgentExecution.tsx`) calls the backend command `execute_agent` ([Chapter 4: Tauri Commands](04_tauri_commands_.md)), passing the agent's ID, the selected project path, and the entered task. +2. The backend receives the call and retrieves the full details of the selected Agent from the database. +3. It creates a record in the `agent_runs` database table. This table keeps track of each individual execution run of an agent, including which agent was run, the task given, the project path, and its current status (pending, running, completed, failed, cancelled). This links back to the run history shown in the `CCAgents.tsx` component and managed by the `AgentRun` struct: + ```rust + // src-tauri/src/commands/agents.rs (Simplified) + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct AgentRun { + pub id: Option, // Database ID for this run + pub agent_id: i64, // Foreign key linking to the Agent + pub agent_name: String, // Stored for convenience + pub agent_icon: String, // Stored for convenience + pub task: String, // The task given for this run + pub model: String, // The model used for this run + pub project_path: String, // The directory where it was executed + pub session_id: String, // The UUID from the Claude Code CLI session + pub status: String, // 'pending', 'running', 'completed', 'failed', 'cancelled' + pub pid: Option, // Process ID if running + pub process_started_at: Option, + pub created_at: String, + pub completed_at: Option, + } + ``` + When the run starts, the status is set to 'running', and the Process ID (PID) is recorded. +4. Based on the Agent's configured permissions (`enable_file_read`, `enable_file_write`, `enable_network`), the backend constructs a sandbox profile. This process involves defining rules that the operating system will enforce to limit what the `claude` process can access or do. This is a core part of the [Sandboxing](06_sandboxing_.md) concept. +5. The backend prepares the command to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). It includes arguments like: + * `-p "the task"` + * `--system-prompt "the agent's system prompt"` + * `--model "the selected model"` + * `--output-format stream-json` (to get structured output) + * `--dangerously-skip-permissions` (since `claudia` manages permissions via the sandbox, it tells `claude` not to ask the user). + * The command is also set to run in the specified project directory. +6. The backend then *spawns* the `claude` process within the sandbox environment. +7. As the `claude` process runs, its standard output (stdout) and standard error (stderr) streams are captured by the backend ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)). +8. The backend processes this output. For JSONL output from Claude Code, it extracts information like message content and session IDs. +9. It emits events back to the frontend (`agent-output`, `agent-error`) using the Tauri event system. +10. The frontend (`AgentExecution.tsx`) listens for these events and updates the displayed messages in real-time. +11. The backend also detects when the `claude` process finishes (either successfully, with an error, or if killed). +12. When the process finishes, the backend updates the `agent_runs` record in the database, setting the status to 'completed', 'failed', or 'cancelled' and recording the completion timestamp. + +Here's a simplified sequence diagram for Agent execution: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI (AgentExecution.tsx) + participant Backend as Tauri Commands (agents.rs) + participant Database as agents.db + participant Sandbox + participant ClaudeCLI as claude binary + + User->>Frontend: Clicks "Execute Agent" + Frontend->>Backend: Call execute_agent(agentId, path, task, model) + Backend->>Database: Read Agent config by ID + Database-->>Backend: Return Agent config + Backend->>Database: Create AgentRun record (status=pending/running) + Database-->>Backend: Return runId + Backend->>Sandbox: Prepare environment based on Agent permissions + Sandbox-->>Backend: Prepared environment/command + Backend->>ClaudeCLI: Spawn process (with task, prompt, model, in sandbox, in project path) + ClaudeCLI-->>Backend: Stream stdout/stderr (JSONL) + Backend->>Frontend: Emit "agent-output" events (parsed messages) + Frontend->>User: Display messages in UI + ClaudeCLI-->>Backend: Process finishes + Backend->>Database: Update AgentRun record (status=completed/failed/cancelled) + Database-->>Backend: Confirmation + Backend->>Frontend: Emit "agent-complete" event + Frontend->>User: Update UI (execution finished) +``` + +This diagram illustrates how the frontend initiates the run, the backend fetches the agent's configuration, prepares the environment (including sandbox rules), launches the `claude` process, captures its output, and updates the UI and database based on the process's progress and completion. + +## Conclusion + +In this chapter, we introduced the concept of **Agents** in `claudia`. We learned that Agents are customizable configurations for the Claude Code CLI, allowing you to define specific roles, instructions (System Prompt), models, and crucially, permissions for different types of tasks. + +We saw how the `claudia` UI allows you to easily create, edit, list, and execute these Agents, and how the backend stores Agent definitions in a local database. We also got a high-level view of the execution process, understanding that `claudia` launches the `claude` binary with the Agent's settings and captures its output. A key part of this is the preparation of a secure execution environment based on the Agent's defined permissions, which introduces the idea of sandboxing. + +Understanding Agents is fundamental, as they are the primary way you'll interact with Claude Code through `claudia` for structured tasks. In the next chapter, we'll zoom out and look at how the different visual parts of the `claudia` application you've seen connect together – diving into [Frontend UI Components](03_frontend_ui_components_.md). + +[Next Chapter: Frontend UI Components](03_frontend_ui_components_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[3]] +``` +(https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentSandboxSettings.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CCAgents.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CreateAgent.tsx) diff --git a/Claudia-docs/V_1/claudia-3.Frontend UI Components.md b/Claudia-docs/V_1/claudia-3.Frontend UI Components.md new file mode 100644 index 00000000..18288e56 --- /dev/null +++ b/Claudia-docs/V_1/claudia-3.Frontend UI Components.md @@ -0,0 +1,328 @@ +# Chapter 3: Frontend UI Components + +Welcome back to the `claudia` tutorial! In [Chapter 1: Session/Project Management](01_session_project_management_.md), we explored how `claudia` keeps track of your conversations. In [Chapter 2: Agents](02_agents_.md), we learned about creating and managing specialized configurations for Claude Code tasks. + +Now, let's shift our focus to what you actually *see* and *interact with* when you use `claudia`: its graphical interface. This interface is built using **Frontend UI Components**. + +## What are Frontend UI Components? + +Imagine building something complex, like a house. You don't start by crafting every tiny screw and nail from raw metal. Instead, you use pre-made bricks, windows, doors, and roof tiles. These are like reusable building blocks. + +Frontend UI Components in `claudia` are exactly like these building blocks, but for the visual parts of the application. They are self-contained pieces of the user interface, like: + +* A **Button** you click. +* A **Card** that displays information (like a project or an agent). +* An **Input** field where you type text. +* A **List** that shows multiple items. +* A **Dialog** box that pops up. + +`claudia` uses a popular web development framework called **React** to build these components. They are written using **TypeScript** (which adds type safety) and styled using **Tailwind CSS** (a way to add styles quickly using special class names). + +The key idea is reusability. Instead of designing a button from scratch every time it's needed, you create a `Button` component once and use it everywhere. This makes the UI consistent and development faster. + +## Building Views by Combining Components + +Just like you combine bricks and windows to build a wall, `claudia` combines different UI components to create full views (pages) of the application. + +For example, the list of projects you saw in Chapter 1 is a view. This view isn't one giant piece of code; it's made by combining: + +* `Button` components (like the "Back to Home" button). +* `Card` components, each displaying information about a single project. +* A `ProjectList` component which *contains* all the individual project `Card`s and handles looping through the list of projects. +* Layout components (like `div`s with Tailwind classes) to arrange everything. + +Let's look at a simplified structure of the `App.tsx` file, which acts like the main blueprint for `claudia`'s views. It decides *which* major component (view) to show based on the current state (`view` variable): + +```typescript +// src/App.tsx (Simplified) +import { useState } from "react"; +import { Button } from "@/components/ui/button"; // Import a UI component +import { Card } from "@/components/ui/card"; // Import another UI component +import { ProjectList } from "@/components/ProjectList"; // Import a view component +import { CCAgents } from "@/components/CCAgents"; // Import another view component +// ... other imports ... + +type View = "welcome" | "projects" | "agents" | "settings" | "claude-code-session"; + +function App() { + const [view, setView] = useState("welcome"); // State variable to control current view + // ... other state variables ... + + const renderContent = () => { + switch (view) { + case "welcome": + // Show the welcome view, using Card and Button components + return ( +
{/* Layout */} + setView("agents")}> {/* Uses Card */} +
+ {/* Icon component */} +

CC Agents

+
+
+ setView("projects")}> {/* Uses Card */} +
+ {/* Icon component */} +

CC Projects

+
+
+
+ ); + + case "agents": + // Show the Agents view, which is handled by the CCAgents component + return setView("welcome")} />; // Uses CCAgents component + + case "projects": + // Show the Projects/Sessions view + return ( +
{/* Layout */} + {/* Uses Button */} + {/* ... displays either ProjectList or SessionList based on selectedProject state ... */} +
+ ); + + // ... other cases for settings, session view, etc. ... + + default: + return null; + } + }; + + return ( +
+ {/* Topbar component */} + {/* Main content area */} +
+ {renderContent()} {/* Renders the selected view */} +
+ {/* ... other global components like dialogs ... */} +
+ ); +} + +export default App; +``` + +As you can see, `App.tsx` doesn't contain the detailed code for *every* button or card. Instead, it imports and uses components like `Button`, `Card`, `CCAgents`, and `ProjectList`. The `renderContent` function simply decides which larger component to display based on the `view` state. + +## How Components Work Together + +Components communicate with each other primarily through **props** (short for properties) and **callbacks** (functions passed as props). + +* **Props:** Data is passed *down* from parent components to child components using props. For example, the `App` component might pass the list of `projects` to the `ProjectList` component. The `ProjectList` component then passes individual `project` objects down to the `Card` components it renders. +* **Callbacks:** When something happens inside a child component (like a button click), it needs to tell its parent. It does this by calling a function that was passed down as a prop (a callback). For example, when a `Card` in the `ProjectList` is clicked, it calls the `onProjectClick` function that was given to it by `ProjectList`. `ProjectList` received this function from `App`. + +Let's revisit the `ProjectList` component from Chapter 1: + +```typescript +// src/components/ProjectList.tsx (Simplified) +// ... imports ... +import { Card, CardContent } from "@/components/ui/card"; // Uses Card component +import type { Project } from "@/lib/api"; + +interface ProjectListProps { + projects: Project[]; // Prop: receives array of project data + onProjectClick: (project: Project) => void; // Prop: receives a function (callback) +} + +export const ProjectList: React.FC = ({ projects, onProjectClick }) => { + return ( +
+ {/* Loops through the projects array received via props */} + {projects.map((project) => ( + // Renders a Card component for each project + onProjectClick(project)}> {/* Calls the onProjectClick callback when clicked */} + {/* Uses CardContent sub-component */} +
+

{project.path}

{/* Displays data received from the project prop */} +

{project.sessions.length} sessions

{/* Displays data from the project prop */} +
+
+
+ ))} +
+ ); +}; +``` + +This component clearly shows: +1. It receives data (`projects` array) and a function (`onProjectClick`) as props. +2. It loops through the `projects` array. +3. For each item, it renders a `Card` component (another UI component). +4. It passes data (`project.path`, `project.sessions.length`) into the `CardContent` to be displayed. +5. It attaches an `onClick` handler to the `Card` that calls the `onProjectClick` callback function, passing the relevant `project` data back up to the parent component (`App` in this case). + +Similarly, the `CCAgents` component from Chapter 2 receives data and callbacks: + +```typescript +// src/components/CCAgents.tsx (Simplified) +// ... imports ... +import { Card, CardContent, CardFooter } from "@/components/ui/card"; // Uses Card components +import { Button } from "@/components/ui/button"; // Uses Button component +// ... types and state ... + +export const CCAgents: React.FC = ({ onBack, className }) => { + // ... state for agents data ... + + // ... useEffect to load agents (calls backend, covered in Chapter 2) ... + + // Callback functions for actions + const handleExecuteAgent = (agent: Agent) => { + // ... navigate to execution view ... + }; + const handleEditAgent = (agent: Agent) => { + // ... navigate to edit view ... + }; + const handleDeleteAgent = (agentId: number) => { + // ... call backend API to delete ... + }; + + return ( +
+ {/* ... Back button using Button component calling onBack prop ... */} + + {/* Agents Grid */} +
+ {/* Loop through agents state */} + {agents.map((agent) => ( + {/* Uses Card */} + {/* Uses CardContent */} + {/* ... display agent icon, name (data from agent state) ... */} + + {/* Uses CardFooter */} + {/* Buttons using Button component, calling local callbacks */} + + + + + + ))} +
+ {/* ... pagination ... */} +
+ ); +}; +``` + +This component shows how UI components (`Card`, `Button`) are used within a larger view component (`CCAgents`). `CCAgents` manages its own state (the list of `agents`) and defines callback functions (`handleExecuteAgent`, `handleEditAgent`, `handleDeleteAgent`) which are triggered by user interaction with the child `Button` components. It also receives an `onBack` prop from its parent (`App`) to navigate back. + +## Common UI Components in `claudia` + +`claudia` uses a set of pre-built, simple UI components provided by a library often referred to as "shadcn/ui" (though integrated directly into the project). You saw some examples in the code: + +* **`Button`**: Used for clickable actions (`components/ui/button.tsx`). +* **`Card`**: Used to group related information with a border and shadow (`components/ui/card.tsx`). It often has `CardHeader`, `CardContent`, and `CardFooter` sub-components for structure. +* **`Input`**: Used for single-line text entry fields (similar to standard HTML ``, used in `CreateAgent`, `AgentExecution`). +* **`Textarea`**: Used for multi-line text entry, like for the system prompt (`components/ui/textarea.tsx`, used in `CreateAgent`). +* **`Switch`**: Used for toggling options on/off, like permissions in the sandbox settings (`components/ui/switch.tsx`, used in `AgentSandboxSettings`). +* **`Label`**: Used to associate text labels with form elements (`components/ui/label.tsx`). +* **`Popover`**: Used to display floating content when a trigger is clicked (`components/ui/popover.tsx`). +* **`Toast`**: Used for temporary notification messages (`components/ui/toast.tsx`). + +You can find these components and others in the `src/components/ui/` directory. Each file defines a single, reusable UI component using React's functional component pattern, TypeScript for typing props, and Tailwind CSS classes for styling. + +For example, the `Button` component (`components/ui/button.tsx`) defines different visual `variant`s (default, destructive, outline, secondary, ghost, link) and `size`s (default, sm, lg, icon) using `class-variance-authority` and then applies the corresponding Tailwind classes (`cn` utility combines class names). When you use ``. + +## How it Works: Under the Hood (Frontend) + +The core idea behind these UI components in React is quite simple: + +1. **They are functions or classes:** A component is essentially a JavaScript/TypeScript function (or class) that receives data as `props`. +2. **They return UI:** This function returns a description of what the UI should look like (React elements, often resembling HTML). +3. **React renders the UI:** React takes this description and efficiently updates the actual web page (the Document Object Model or DOM) to match. +4. **State for interactivity:** Some components manage their own internal data called `state` (e.g., an input component's text value, whether a dialog is open). When state changes, the component re-renders. +5. **Event Handlers:** Components respond to user interactions (like clicks, typing) by calling functions defined within them or received via props (callbacks). + +The process looks like this: + +```mermaid +graph TD + A[App.tsx] --> B(Passes props like projects, callbacks like handleProjectClick) + B --> C{ProjectList Component} + C --> D(Iterates through projects, passes individual project + onProjectClick to Cards) + D --> E{Card Component (for a single project)} + E --> F(Receives project data + onProjectClick) + F -- Displays Data --> G[UI on screen (a Card)] + G -- User Clicks Card --> H(onClick handler in Card) + H --> I(Calls the onProjectClick callback received via props) + I --> J(Returns the clicked project data) + J --> C(ProjectList receives data) + C --> K(Calls the onProjectClick callback received via props) + K --> A(App.tsx receives clicked project data) + A -- Updates state (e.g., setSelectedProject) --> A + A -- Re-renders with new view --> L[New UI on screen (e.g., SessionList)] +``` + +This diagram shows the flow of data (props) and events (callbacks) that allows components to work together to create a dynamic interface. `App.tsx` is at the top, managing the main state (`view`, `selectedProject`). It passes data and functions down to its children (`ProjectList`). `ProjectList` loops and renders more children (`Card`). When a `Card` receives a user action, it calls a function passed down (`onProjectClick`), sending relevant data back up the chain, which triggers state changes in the parent (`App`), leading to a re-render and a different view being displayed. + +## Conclusion + +In this chapter, we explored Frontend UI Components, the reusable building blocks that form the visual interface of `claudia`. We learned that these components, built with React, TypeScript, and Tailwind CSS, are combined like Lego bricks to create complex views like project lists, agent managers, and the main session interface. + +We saw how components receive data through `props` and communicate back to their parents using `callbacks`. This system allows the UI to be modular, consistent, and maintainable. Understanding these components is key to seeing how `claudia` presents information and interacts with the user. + +In the next chapter, we'll bridge the gap between the frontend UI components and the backend Rust logic by learning about [Tauri Commands](04_tauri_commands_.md). These commands are the communication layer that allows the components to ask the backend for data (like listing projects) or request actions (like executing an agent). + +[Next Chapter: Tauri Commands](04_tauri_commands_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/App.tsx), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/index.ts), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/badge.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/button.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/card.tsx), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/popover.tsx), [[7]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/textarea.tsx) +``` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-4.Tauri Commands.md b/Claudia-docs/V_1/claudia-4.Tauri Commands.md new file mode 100644 index 00000000..b1cb0b4c --- /dev/null +++ b/Claudia-docs/V_1/claudia-4.Tauri Commands.md @@ -0,0 +1,299 @@ +# Chapter 4: Tauri Commands + +Welcome back to the `claudia` tutorial! In [Chapter 3: Frontend UI Components](03_frontend_ui_components_.md), we explored the visual building blocks that make up `claudia`'s interface, like Buttons and Cards, and how they communicate with each other using props and callbacks. + +But those frontend components, written in TypeScript/JavaScript, can't directly talk to your operating system. They can't read files, launch other programs, or perform heavy computations safely and efficiently. This is where the backend, written in Rust, comes in. + +We need a way for the frontend UI (your browser-like window) to ask the powerful native backend to do things for it. That communication bridge is what **Tauri Commands** are all about. + +## What are Tauri Commands? + +Think of Tauri Commands as a special "phone line" or "API" that connects the frontend world (where the user clicks buttons and sees things) to the backend world (where the native code runs). + +When the user clicks a button in `claudia`'s UI, and that button needs to do something like: + +* List your projects (which requires reading the file system). +* Create a new Agent (which requires saving to a database). +* Execute a Claude Code session (which requires launching a separate process). + +...the frontend can't do this itself. Instead, it calls a specific **Tauri Command** that lives in the Rust backend. The backend command performs the requested action and then sends the result back to the frontend. + +**In simple terms:** + +* Tauri Commands are functions in the Rust backend. +* They are specifically marked so that Tauri knows they can be called from the frontend. +* The frontend calls these functions using a special `invoke` mechanism provided by Tauri. +* This allows the frontend to trigger native actions and get data from the backend. + +This separation keeps the UI responsive and safe, while the backend handles the heavy lifting and privileged operations. + +## How to Call a Tauri Command from the Frontend + +In `claudia`'s frontend (written in TypeScript), you call a backend command using the `invoke` function from the `@tauri-apps/api/core` library. + +The `invoke` function is straightforward: + +```typescript +import { invoke } from "@tauri-apps/api/core"; + +// ... later in your component or API helper ... + +async function exampleCall() { + try { + // Call the command named 'list_projects' + // If the command takes arguments, pass them as the second parameter (an object) + const result = await invoke("list_projects"); + + console.log("Projects received:", result); // Handle the result + // result will be the value returned by the Rust function + + } catch (error) { + console.error("Error calling list_projects:", error); // Handle errors + } +} + +// To actually trigger it, you might call exampleCall() in response to a button click or when a page loads. +``` + +Let's look at the `src/lib/api.ts` file, which we briefly mentioned in previous chapters. This file provides a cleaner way to call backend commands instead of using `invoke` directly everywhere. It defines functions like `listProjects`, `getProjectSessions`, `listAgents`, `createAgent`, `executeAgent`, etc., which wrap the `invoke` calls. + +Here's how the `listProjects` function is defined in `src/lib/api.ts`: + +```typescript +// src/lib/api.ts (Simplified) +import { invoke } from "@tauri-apps/api/core"; +// ... other imports and type definitions ... + +/** + * Represents a project in the ~/.claude/projects directory + */ +export interface Project { + // ... project fields ... +} + +/** + * API client for interacting with the Rust backend + */ +export const api = { + /** + * Lists all projects in the ~/.claude/projects directory + * @returns Promise resolving to an array of projects + */ + async listProjects(): Promise { // Defines a friendly TypeScript function + try { + // Calls the actual Tauri command named "list_projects" + return await invoke("list_projects"); + } catch (error) { + console.error("Failed to list projects:", error); + throw error; // Re-throw the error for the caller to handle + } + }, + + // ... other API functions like getProjectSessions, listAgents, etc. +}; +``` + +Now, in a frontend component like `ProjectList.tsx` or its parent view, instead of `invoke`, you'll see code calling `api.listProjects()`: + +```typescript +// src/components/ProjectList.tsx (Simplified - from Chapter 1) +import React, { useEffect, useState } from 'react'; +// ... other imports ... +import { api, type Project } from "@/lib/api"; // Import the api client and types + +// ... component definition ... + +export const ProjectList: React.FC = ({ onProjectClick }) => { + const [projects, setProjects] = useState([]); + // ... other state ... + + useEffect(() => { + // Fetch projects from the backend when the component loads + const loadProjects = async () => { + try { + // Call the backend command via the api helper + const projectsList = await api.listProjects(); + setProjects(projectsList); // Update the component's state with the data + } catch (err) { + console.error("Failed to load projects:", err); + } + }; + loadProjects(); // Call the function to load data + }, []); // Empty dependency array means this runs once after initial render + + // ... render function using the 'projects' state ... + // Uses projects.map to display each project (as shown in Chapter 1) +}; +``` + +This shows the typical pattern: A frontend component needs data, so it calls a function in `src/lib/api.ts` (like `api.listProjects`), which in turn uses `invoke` to call the corresponding backend command. The component then uses the received data (`projectsList`) to update its state and render the UI. + +## How to Define a Tauri Command in the Backend (Rust) + +Now, let's look at the other side: how the backend tells Tauri that a specific Rust function can be called as a command. + +This is done using the `#[tauri::command]` attribute right above the function definition. These command functions typically live in modules within the `src-tauri/src/commands/` directory (like `claude.rs` or `agents.rs`). + +Here's the simplified Rust code for the `list_projects` command, located in `src-tauri/src/commands/claude.rs`: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +use tauri::command; +use serde::{Serialize, Deserialize}; // Needed for sending data back + +// Define the structure that will be sent back to the frontend +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub id: String, + pub path: String, + pub sessions: Vec, + pub created_at: u64, +} + +// Mark this function as a Tauri command +#[command] +pub async fn list_projects() -> Result, String> { + // ... Code to find ~/.claude and read project directories ... + // This is where the file system access happens (backend logic) + + let mut projects = Vec::new(); + + // Simplified: Imagine we found some projects and populated the 'projects' vector + // For a real implementation, see the detailed code snippet in Chapter 1 + + // Example placeholder data: + projects.push(Project { + id: "encoded-path-1".to_string(), + path: "/path/to/my/project1".to_string(), + sessions: vec!["session1_id".to_string(), "session2_id".to_string()], + created_at: 1678886400, // Example timestamp + }); + projects.push(Project { + id: "encoded-path-2".to_string(), + path: "/path/to/my/project2".to_string(), + sessions: vec!["session3_id".to_string()], + created_at: 1678972800, // Example timestamp + }); + + + // Return the vector of Project structs. + // Result is often used for commands that might fail. + // Tauri automatically serializes Vec into JSON for the frontend. + Ok(projects) +} + +// ... other commands defined in this file ... +``` + +Key points here: + +1. `#[tauri::command]`: This attribute is essential. It tells Tauri to generate the necessary code to make this Rust function callable from the frontend JavaScript. +2. `pub async fn`: Commands are typically `async` functions because they often perform non-blocking operations (like reading files, launching processes) that shouldn't block the main UI thread. They must also be `pub` (public) so Tauri can access them. +3. `Result, String>`: This is the return type. `Result` is a standard Rust type for handling operations that can either succeed (`Ok`) or fail (`Err`). Here, on success, it returns a `Vec` (a list of `Project` structs); on failure, it returns a `String` error message. Tauri handles converting this Rust `Result` into a JavaScript Promise that resolves on `Ok` and rejects on `Err`. +4. `#[derive(Serialize, Deserialize)]`: Any custom data structures (like `Project` here) that you want to send between the frontend and backend must be able to be converted to/from a common format like JSON. `serde` is a Rust library for this, and deriving `Serialize` and `Deserialize` (for data going back and forth) makes this automatic. + +## Registering Commands + +Finally, for Tauri to know about your command functions, they need to be registered in the main application entry point, `src-tauri/src/main.rs`. + +In `src-tauri/src/main.rs`, there's a section using `tauri::generate_handler!` that lists all the command functions that the frontend is allowed to call: + +```rust +// src-tauri/src/main.rs (Simplified) +// ... imports ... + +mod commands; // Import your commands module + +use commands::claude::{ + list_projects, // Import the specific command functions + get_project_sessions, + // ... import other claude commands ... +}; +use commands::agents::{ + list_agents, // Import agent commands + create_agent, + execute_agent, + // ... import other agent commands ... +}; +// ... import commands from other modules like sandbox, usage, mcp ... + +fn main() { + // ... setup code ... + + tauri::Builder::default() + // ... plugins and setup ... + .invoke_handler(tauri::generate_handler![ // **This is where commands are registered!** + list_projects, // List the name of each command function + get_project_sessions, + list_agents, + create_agent, + execute_agent, + // ... list all other commands you want to expose ... + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +The `tauri::generate_handler! macro` takes a list of function names that are marked with `#[tauri::command]`. It generates the code needed for Tauri's core to receive `invoke` calls from the frontend and route them to the correct Rust function. If a command isn't listed here, the frontend can't call it. + +## How it Works: Under the Hood + +Let's visualize the flow when the frontend calls a Tauri Command. + +Imagine the user is on the Projects screen, and the `ProjectList` component needs the list of projects: + +```mermaid +sequenceDiagram + participant FrontendUI as Frontend UI (ProjectList.tsx) + participant FrontendAPI as Frontend API (api.ts) + participant TauriCore as Tauri Core + participant BackendCommands as Backend Commands (claude.rs) + participant Filesystem as Filesystem + + FrontendUI->>FrontendAPI: Need projects list + FrontendAPI->>TauriCore: invoke("list_projects") + Note over TauriCore: Tauri routes call to registered handler + TauriCore->>BackendCommands: Call list_projects() function + BackendCommands->>Filesystem: Read ~/.claude/projects + Filesystem-->>BackendCommands: Return directory contents + BackendCommands->>BackendCommands: Process data (create Project structs) + BackendCommands-->>TauriCore: Return Result, String> + TauriCore-->>FrontendAPI: Resolve invoke Promise with Vec + FrontendAPI-->>FrontendUI: Return projects data + FrontendUI->>FrontendUI: Update state with projects data + FrontendUI->>FrontendUI: Render UI (display projects) +``` + +1. The `ProjectList` component (Frontend UI) decides it needs the list of projects, perhaps in a `useEffect` hook when it mounts. +2. It calls `api.listProjects()` (Frontend API wrapper). +3. `api.listProjects()` calls `invoke("list_projects")`, which sends a message to the Tauri Core. +4. The Tauri Core receives the message "call command 'list\_projects'" and looks up the corresponding registered Rust function. +5. The Tauri Core executes the `list_projects()` function in the Backend Commands module. +6. The Rust function performs its logic, which involves interacting with the Filesystem (reading directories and files). +7. The Filesystem returns the necessary data to the Rust function. +8. The Rust function processes this data and constructs the `Vec` result. +9. The Rust function returns the `Result, String>`. Tauri automatically serializes the `Vec` into JSON. +10. The Tauri Core receives the result and sends it back to the frontend process. +11. The Promise returned by the initial `invoke` call in `api.ts` resolves with the JSON data, which Tauri automatically deserializes back into a TypeScript `Project[]` array. +12. `api.listProjects()` returns this array to the `ProjectList` component. +13. The `ProjectList` component updates its internal state, triggering React to re-render the component, displaying the list of projects on the screen. + +This same pattern is used for almost all interactions where the frontend needs to get information or trigger actions in the backend. For example, when you click "Execute" for an Agent (as seen in Chapter 2), the `AgentExecution.tsx` component calls `api.executeAgent()`, which calls the backend `execute_agent` command, which then launches the `claude` binary (as we'll see in [Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). + +## Conclusion + +In this chapter, we learned about **Tauri Commands**, the essential communication layer that bridges the gap between the frontend UI (built with React/TypeScript) and the native backend logic (written in Rust). + +We saw how the frontend uses `invoke` (often wrapped by helpful functions in `src/lib/api.ts`) to call named backend commands, passing arguments and receiving results via Promises. We also saw how backend Rust functions are defined using `#[tauri::command]`, must be `pub async fn`, return a `Result`, and how data is serialized using `serde`. Finally, we looked at how these commands are registered in `src-tauri/src/main.rs` using `tauri::generate_handler!`. + +Understanding Tauri Commands is crucial because they are the fundamental way `claudia`'s UI interacts with the powerful, native capabilities provided by the Rust backend. This mechanism allows the frontend to stay focused on presentation while relying on the backend for tasks like file system access, process management, and database interaction. + +In the next chapter, we'll delve into the very core of `claudia`'s function: how it interacts with the command-line `claude` binary to run sessions and execute tasks. + +[Next Chapter: Claude CLI Interaction](05_claude_cli_interaction_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/mod.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/lib.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/main.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/lib/api.ts) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-5.Claude CLI interaction.md b/Claudia-docs/V_1/claudia-5.Claude CLI interaction.md new file mode 100644 index 00000000..e2e64675 --- /dev/null +++ b/Claudia-docs/V_1/claudia-5.Claude CLI interaction.md @@ -0,0 +1,373 @@ +# Chapter 5: Claude CLI Interaction + +Welcome back to the `claudia` tutorial! In our previous chapters, we learned about managing your work with Claude Code through [Session/Project Management](01_session_project_management_.md), creating specialized [Agents](02_agents_.md) to define how Claude should behave, how the [Frontend UI Components](03_frontend_ui_components_.md) like buttons and lists build the interface, and how [Tauri Commands](04_tauri_commands_.md) allow the frontend (TypeScript/React) to talk to the backend (Rust). + +Now, let's dive into the core action: how `claudia` actually makes the powerful `claude` command-line tool run and communicate with it. This chapter is all about the **Claude CLI Interaction** layer. + +## The Problem: GUI Needs to Talk to CLI + +You're using `claudia`, which is a beautiful graphical application. You click buttons, type in text boxes, and see output in a nice interface. But the actual intelligence, the part that runs your requests and generates code or text, is the `claude` command-line interface (CLI) tool that you installed separately. + +So, how does `claudia`'s backend, written in Rust, tell the `claude` CLI, which is a separate program running on your computer, what to do? How does it get the response back in real-time to show you? + +This is exactly what the Claude CLI Interaction part of `claudia` handles. It's the bridge between the graphical application and the underlying CLI tool. + +Imagine you're the director of an orchestra (`claudia`). You have a conductor's stand (the UI), but the music is played by the musicians (`claude`). You need a way to signal to the musicians what piece to play, at what tempo, and capture their performance to share with the audience. `claudia`'s CLI Interaction is your way of signaling to the `claude` process and listening to its "music" (the output). + +## What the Claude CLI Interaction Does + +The core function of this layer in `claudia`'s backend is to: + +1. **Find the `claude` binary:** Figure out where the `claude` executable is located on your system. +2. **Prepare the command:** Build the command line that needs to be run, including the `claude` binary path and all the necessary arguments (like the prompt, model, system prompt, etc.). +3. **Spawn the process:** Start the `claude` binary as a separate process. +4. **Control the environment:** Set the working directory for the `claude` process (the project path) and potentially adjust its environment variables (like the PATH). +5. **Manage sandboxing (Optional but important):** If sandboxing is enabled, ensure the `claude` process runs within the defined security restrictions (more on this in [Chapter 6: Sandboxing](06_sandboxing_.md)). +6. **Capture output:** Get the standard output (stdout) and standard error (stderr) streams from the running `claude` process in real-time. +7. **Process output:** Take the raw output (which is in a special JSONL format for Claude Code) and process it. +8. **Report status/output:** Send the processed output and status updates (running, complete, failed) back to the frontend so the user interface can update. +9. **Manage process lifecycle:** Keep track of the running process and handle requests to stop or kill it. + +## Triggering a Claude Code Run from the Frontend + +You've already seen in Chapter 4 how frontend components use `api` functions to call backend commands. This is how you initiate a Claude Code run. + +Whether you're executing an Agent (from `AgentExecution.tsx`) or starting/continuing a direct session (from `ClaudeCodeSession.tsx`), the frontend makes a call to a specific backend command responsible for launching `claude`. + +Here's a simplified look at how `AgentExecution.tsx` initiates a run: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +import { api, type Agent } from "@/lib/api"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; + +// ... component definition ... + +const handleExecute = async () => { + // ... validation and state updates (isLoading, etc.) ... + + try { + // Set up event listeners first (covered in Chapter 7) + // These listeners will receive output and status updates from the backend + const outputUnlisten = await listen("agent-output", (event) => { + // Process received output line (JSONL) + // ... update messages state ... + }); + const completeUnlisten = await listen("agent-complete", (event) => { + // Process completion status + // ... update isRunning state ... + }); + // ... store unlisten functions ... + + // Call the backend command to execute the agent + // This command prepares and spawns the 'claude' process + await api.executeAgent(agent.id!, projectPath, task, model); + + } catch (err) { + console.error("Failed to execute agent:", err); + // ... handle error ... + } +}; + +// ... render function with button calling handleExecute ... +``` + +And here's a similar pattern from `ClaudeCodeSession.tsx` for starting a new session: + +```typescript +// src/components/ClaudeCodeSession.tsx (Simplified) +// ... imports ... +import { api, type Session } from "@/lib/api"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; + +// ... component definition ... + +const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => { + // ... validation and state updates (isLoading, etc.) ... + + try { + // Add the user message to the UI immediately + // ... update messages state ... + + // Clean up old listeners, set up new ones (for "claude-output", "claude-complete") + // ... setup listeners (covered in Chapter 7) ... + + // Call the appropriate backend command + // This command prepares and spawns the 'claude' process + if (isFirstPrompt && !session) { + await api.executeClaudeCode(projectPath, prompt, model); // New session + } else if (session && isFirstPrompt) { + await api.resumeClaudeCode(projectPath, session.id, prompt, model); // Resume session + } else { + await api.continueClaudeCode(projectPath, prompt, model); // Continue conversation + } + + } catch (err) { + console.error("Failed to send prompt:", err); + // ... handle error ... + } +}; + +// ... render function with FloatingPromptInput component calling handleSendPrompt ... +``` + +These snippets show that from the frontend's perspective, starting a Claude Code interaction is simply calling a backend API function (a Tauri Command wrapper) and then listening for events that the backend sends back as the process runs and finishes. + +## How it Works: Under the Hood (Backend) + +When the backend receives a Tauri command like `execute_agent` or `execute_claude_code`, it performs a series of steps to launch and manage the `claude` process. + +Here's a simplified step-by-step flow: + +1. **Find the `claude` executable:** The backend needs the full path to the `claude` binary. It looks in common installation locations and potentially a path saved in `claudia`'s settings. +2. **Determine process parameters:** It gathers the necessary information for the command: the prompt (`-p`), the system prompt (`--system-prompt`, from the Agent config or CLAUDE.md), the model (`--model`), the output format (`--output-format stream-json` is crucial for real-time processing), flags like `--verbose` and `--dangerously-skip-permissions` (since `claudia` handles permissions via sandboxing), and the working directory (`--current-dir` or set via `Command`). +3. **Prepare Sandbox (if enabled):** Based on Agent permissions or global settings, the backend constructs sandbox rules using the `gaol` library. This involves defining what file paths (`file_read_all`, `file_write_all`) and network connections (`network_outbound`) the `claude` process is allowed to make. This is tightly linked to the actual command execution. +4. **Build the Command object:** Rust's standard library (and the `tokio` library for asynchronous operations) provides a `Command` struct to build process commands. The backend creates a `Command` instance, sets the `claude` binary path, adds all the arguments, sets the working directory (`current_dir`), and configures standard input/output (`stdin`, `stdout`, `stderr`) to be piped so the backend can capture them. +5. **Spawn the child process:** The `Command` object is executed using a method like `spawn()`. This starts the `claude` process and gives the backend a handle to it (a `Child` object). +6. **Capture Output Streams:** The `stdout` and `stderr` streams of the child process, which were configured to be piped, are now available as asynchronous readers. The backend spawns separate asynchronous tasks (using `tokio::spawn`) to continuously read lines from these streams. +7. **Process and Emit:** As each line of output (usually a JSON object in the JSONL format) or error arrives, the reading tasks process it (e.g., parse JSON, extract relevant data) and immediately emit it as a Tauri event back to the frontend (`agent-output`, `claude-output`, `agent-error`, `claude-error`). This provides the real-time streaming experience. +8. **Monitor Completion:** The backend also has a task that waits for the `claude` process to finish (`child.wait().await`). When it exits, the task notifies the frontend (e.g., via `agent-complete`, `claude-complete`) and potentially updates internal state or a database record (like the `agent_runs` table for Agents). +9. **Handle Cancellation:** If the user requests to stop the process (e.g., clicking a "Stop" button for an Agent run), the backend uses the process ID (PID) to send a termination signal (`kill`). + +Here's a sequence diagram showing the flow for a standard `execute_claude_code` call: + +```mermaid +sequenceDiagram + participant FrontendUI as Frontend UI (ClaudeCodeSession.tsx) + participant FrontendAPI as Frontend API (api.ts) + participant TauriCore as Tauri Core + participant BackendCommands as Backend Commands (claude.rs) + participant OS as Operating System + participant ClaudeCLI as claude binary + + FrontendUI->>FrontendAPI: User submits prompt (call executeClaudeCode) + FrontendAPI->>TauriCore: invoke("execute_claude_code", { prompt, path, model }) + Note over TauriCore: Tauri routes call + TauriCore->>BackendCommands: Call execute_claude_code() + BackendCommands->>BackendCommands: Find claude binary path (find_claude_binary) + BackendCommands->>BackendCommands: Prepare Command object (args, cwd, piped streams) + BackendCommands->>OS: Spawn process (claude binary) + OS-->>BackendCommands: Return Child process handle + BackendCommands->>BackendCommands: Spawn tasks to read stdout/stderr + loop While ClaudeCLI is running & produces output + ClaudeCLI-->>OS: Write to stdout/stderr pipe + OS-->>BackendCommands: Data available in pipe + BackendCommands->>BackendCommands: Read & process output line + BackendCommands->>TauriCore: Emit "claude-output" or "claude-error" event + TauriCore-->>FrontendUI: Receive event data + FrontendUI->>FrontendUI: Display output line in UI + end + ClaudeCLI-->>OS: Process exits + OS-->>BackendCommands: Process termination status + BackendCommands->>BackendCommands: Task waits for process exit + BackendCommands->>TauriCore: Emit "claude-complete" event + TauriCore-->>FrontendUI: Receive event + FrontendUI->>FrontendUI: Update UI (execution finished) +``` + +This diagram visually outlines how the request flows from the frontend to the backend, how the backend launches the separate `claude` process via the OS, how output streams back through the backend and Tauri, and finally how the frontend is updated in real-time. + +## Diving into the Backend Code + +Let's look at some key parts of the Rust code in `src-tauri/src/commands/claude.rs` and `src-tauri/src/commands/agents.rs` that handle this process interaction. + +First, finding the binary and setting up the environment: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::process::Command; +use std::process::Stdio; +use tauri::{AppHandle, Emitter, Manager}; + +// This function tries to locate the 'claude' executable +fn find_claude_binary(app_handle: &AppHandle) -> Result { + // ... logic to check settings, common paths, 'which' command ... + // Returns the found path or an error + Ok("path/to/claude".to_string()) // Simplified +} + +// This function creates a Tokio Command object, setting environment variables +fn create_command_with_env(program: &str) -> Command { + let mut cmd = Command::new(program); + + // Inherit essential environment variables like PATH, HOME, etc. + // This helps the 'claude' binary find Node.js and other dependencies + for (key, value) in std::env::vars() { + // ... filtering for safe/necessary variables ... + cmd.env(&key, &value); + } + + cmd // Return the Command object +} + +// ... rest of the file ... +``` + +`find_claude_binary` is crucial to ensure `claudia` can actually find the executable regardless of how it was installed. `create_command_with_env` is a helper to build the base command object and ensure it inherits essential environment variables, which is often necessary for `claude` to run correctly, especially on macOS GUI launches where the default PATH is minimal. + +Next, the core logic for spawning the process and handling its output streams. This is extracted into a helper function `spawn_claude_process` used by `execute_claude_code`, `continue_claude_code`, and `resume_claude_code`. A similar pattern exists within `execute_agent`. + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::io::{AsyncBufReadExt, BufReader}; + +/// Helper function to spawn Claude process and handle streaming +async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> { + log::info!("Spawning Claude process..."); + + // Configure stdout and stderr to be piped + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + // Spawn the process asynchronously + let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?; + + log::info!("Claude process spawned successfully with PID: {:?}", child.id()); + + // Take the piped stdout and stderr handles + let stdout = child.stdout.take().ok_or("Failed to get stdout")?; + let stderr = child.stderr.take().ok_or("Failed to get stderr")?; + + // Create asynchronous buffered readers for the streams + let stdout_reader = BufReader::new(stdout); + let stderr_reader = BufReader::new(stderr); + + // Spawn a separate task to read and process stdout lines + let app_handle_stdout = app.clone(); + tokio::spawn(async move { + let mut lines = stdout_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + log::debug!("Claude stdout: {}", line); + // Emit the line as an event to the frontend + // Frontend listens for "claude-output" + let _ = app_handle_stdout.emit("claude-output", &line); + } + log::info!("Finished reading Claude stdout."); + }); + + // Spawn a separate task to read and process stderr lines + let app_handle_stderr = app.clone(); + tokio::spawn(async move { + let mut lines = stderr_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + log::error!("Claude stderr: {}", line); + // Emit error lines as an event to the frontend + // Frontend listens for "claude-error" + let _ = app_handle_stderr.emit("claude-error", &line); + } + log::info!("Finished reading Claude stderr."); + }); + + // Spawn a task to wait for the process to finish + let app_handle_complete = app.clone(); + tokio::spawn(async move { + match child.wait().await { // Wait for the process to exit + Ok(status) => { + log::info!("Claude process exited with status: {}", status); + // Emit a completion event to the frontend + // Frontend listens for "claude-complete" + tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Small delay + let _ = app_handle_complete.emit("claude-complete", status.success()); + } + Err(e) => { + log::error!("Failed to wait for Claude process: {}", e); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Small delay + let _ = app_handle_complete.emit("claude-complete", false); // Indicate failure + } + } + }); + + Ok(()) +} + +// ... rest of the file with commands like execute_claude_code calling spawn_claude_process ... +``` + +This `spawn_claude_process` function is the heart of the interaction. It sets up the communication channels (`stdout`, `stderr` pipes), starts the `claude` process, and then uses `tokio::spawn` to run multiple things concurrently: reading output, reading errors, and waiting for the process to finish. Each piece of output or status change triggers an `app.emit` call, sending the information via Tauri's event system back to the frontend. + +Finally, handling cancellation for Agent runs involves finding the process ID (PID) and sending a signal. + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +// ... imports ... +use rusqlite::{params, Connection, Result as SqliteResult}; // For database access + +/// Kill a running agent session +#[tauri::command] +pub async fn kill_agent_session( + db: State<'_, AgentDb>, // Access to the database state + run_id: i64, +) -> Result { + log::info!("Attempting to kill agent session run: {}", run_id); + + let pid_result = { + let conn = db.0.lock().map_err(|e| e.to_string())?; + // Retrieve the PID from the database for the specific run + conn.query_row( + "SELECT pid FROM agent_runs WHERE id = ?1 AND status = 'running'", + params![run_id], + |row| row.get::<_, Option>(0) + ) + .map_err(|e| e.to_string())? + }; + + if let Some(pid) = pid_result { + log::info!("Found PID {} for run {}", pid, run_id); + // Use the standard library to send a kill signal + // Behavior differs slightly on Windows vs Unix-like systems + let kill_result = if cfg!(target_os = "windows") { + std::process::Command::new("taskkill") + .args(["/F", "/PID", &pid.to_string()]) // Force kill by PID + .output() + } else { + std::process::Command::new("kill") + .args(["-TERM", &pid.to_string()]) // Send termination signal + .output() + }; + + match kill_result { + Ok(output) if output.status.success() => { + log::info!("Successfully sent kill signal to process {}", pid); + } + Ok(_) => { + log::warn!("Kill command failed for PID {}", pid); + } + Err(e) => { + log::warn!("Failed to execute kill command for PID {}: {}", pid, e); + } + } + } else { + log::warn!("No running PID found for run {}", run_id); + } + + // Update the database to mark the run as cancelled, regardless of kill success + let conn = db.0.lock().map_err(|e| e.to_string())?; + let updated = conn.execute( + "UPDATE agent_runs SET status = 'cancelled', completed_at = CURRENT_TIMESTAMP WHERE id = ?1 AND status = 'running'", + params![run_id], + ).map_err(|e| e.to_string())?; + + Ok(updated > 0) // Return true if a record was updated +} + +// ... rest of the file ... +``` + +This `kill_agent_session` command looks up the process ID associated with the agent run in the database, then attempts to terminate that process using system commands (`kill` or `taskkill`). Finally, it updates the database record for the run to mark it as "cancelled". + +## Conclusion + +In this chapter, we explored the **Claude CLI Interaction** layer, which is fundamental to how `claudia` functions. We learned that this part of the backend is responsible for finding the `claude` binary, preparing the command with all necessary arguments, spawning the `claude` process, setting its environment (including sandboxing), capturing its output and errors in real-time, and managing its lifecycle until completion or cancellation. + +We saw how frontend calls to Tauri Commands trigger this process, how the backend uses Rust's `Command` features and `tokio` for asynchronous stream handling, and how output and status updates are sent back to the frontend via Tauri events, enabling the real-time display of results. This interaction layer effectively turns the `claude` CLI into a powerful engine driven by the user-friendly `claudia` graphical interface. + +Next, we'll take a closer look at a critical aspect touched upon in this chapter: **Sandboxing**. We'll see how `claudia` uses operating system features to limit the permissions of the `claude` process, enhancing security when running code or interacting with your file system. + +[Next Chapter: Sandboxing](06_sandboxing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/claude.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/executor.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-6.Sandboxing.md b/Claudia-docs/V_1/claudia-6.Sandboxing.md new file mode 100644 index 00000000..cb21212a --- /dev/null +++ b/Claudia-docs/V_1/claudia-6.Sandboxing.md @@ -0,0 +1,396 @@ +# Chapter 6: Sandboxing + +Welcome back to the `claudia` tutorial! In our previous chapters, we've learned about organizing your work with [Session/Project Management](01_session_project_management_.md), defining specialized assistants with [Agents](02_agents_.md), how the [Frontend UI Components](03_frontend_ui_components_.md) create the user interface, how [Tauri Commands](04_tauri_commands_.md) connect the frontend and backend, and how `claudia` interacts with the `claude` command-line tool in [Claude CLI Interaction](05_claude_cli_interaction_.md). + +Now, let's talk about a crucial aspect of security: **Sandboxing**. + +## The Problem: Running Untrusted Code + +When you use `claudia` to run an Agent or a direct Claude Code session, you are essentially asking the application to launch the separate `claude` binary on your computer. This `claude` binary can then execute code or perform actions based on the instructions it receives from Claude (and indirectly, from you). + +Imagine you ask Claude to "write a script to delete all files in `/tmp`". While this is a harmless directory, what if you accidentally asked it to delete files in your `/Users/yourname/Documents` folder, or worse, system files? Or what if a malicious instruction somehow slipped into the context? + +Running external processes, especially ones that might execute code or interact with your file system and network, introduces a security risk. By default, any program you run has the same permissions as you do. It could potentially read your sensitive files, delete important data, or connect to unwanted places on the internet. + +This is where **Sandboxing** comes in. + +## What is Sandboxing? + +Sandboxing is like putting a protective barrier around the process that `claudia` launches (the `claude` binary). It creates a restricted environment that limits what that process can see and do on your computer, based on a predefined set of rules. + +Think of it like giving the AI a restricted workspace. You give it access only to the specific tools and areas it needs to do its job for this particular task, and nothing more. + +In `claudia`, sandboxing is primarily used to control the `claude` process's access to: + +1. **File System:** Prevent reading or writing files outside of specific allowed directories (like your project folder). +2. **Network:** Prevent making unwanted connections to the internet or local network. +3. **System Information:** Limit access to potentially sensitive system details. + +By default, `claudia` aims to run Agents and sessions within a sandbox, giving you control over their permissions. + +## Sandboxing with Agents + +The primary way you interact with sandboxing settings in `claudia` is through the **Agent configuration**. As you saw in [Chapter 2: Agents](02_agents_.md), each Agent has specific permission toggles. + +Let's revisit the simplified `AgentSandboxSettings.tsx` component from Chapter 2: + +```typescript +// src/components/AgentSandboxSettings.tsx (Simplified) +// ... imports ... +import { Switch } from "@/components/ui/switch"; +// ... other components ... + +export const AgentSandboxSettings: React.FC = ({ + agent, + onUpdate, + className +}) => { + // ... handleToggle function ... + + return ( + // ... Card and layout ... + {/* Master sandbox toggle */} +
+ + handleToggle('sandbox_enabled', checked)} + /> +
+ + {/* Permission toggles - conditional render */} + {agent.sandbox_enabled && ( +
+ {/* File Read Toggle */} +
+ + handleToggle('enable_file_read', checked)} + /> +
+ {/* File Write Toggle */} +
+ + handleToggle('enable_file_write', checked)} + /> +
+ {/* Network Toggle */} +
+ + handleToggle('enable_network', checked)} + /> +
+
+ )} + {/* ... Warning when sandbox disabled ... */} + // ... end Card ... + ); +}; +``` + +These switches directly control whether the `claude` process launched *by this specific Agent* will be sandboxed and what high-level permissions it will have: + +* **Enable Sandbox:** The main switch. If off, sandboxing is disabled for this Agent, and the process runs with full permissions (like running `claude` directly in your terminal). This should be used with caution. +* **File Read Access:** If enabled, the sandboxed process can read files. Without this, it might not even be able to read the source files in your project directory. +* **File Write Access:** If enabled, the sandboxed process can create or modify files. +* **Network Access:** If enabled, the sandboxed process can make outbound network connections (e.g., accessing APIs, cloning repositories). + +These Agent-specific toggles allow you to quickly define a security posture tailored to the Agent's purpose. A "Code Reader" Agent might only need File Read. A "Code Fixer" might need File Read and Write. A "Web API Helper" might need Network Access. + +## How it Works: Under the Hood + +When you click "Execute" for an Agent or start a session, `claudia`'s backend takes the Agent's sandbox settings (or default settings for direct sessions) and translates them into concrete rules that the operating system can enforce. + +`claudia` uses system-level sandboxing mechanisms through a library called `gaol`. `gaol` provides a way for the parent process (`claudia`'s backend) to define restrictions for a child process (`claude`). + +Here's a simplified look at the steps when `claudia` launches a sandboxed `claude` process: + +1. **Get Agent Permissions:** The backend fetches the selected Agent's configuration from the database, including the `sandbox_enabled`, `enable_file_read`, `enable_file_write`, and `enable_network` fields. +2. **Load Sandbox Profile & Rules:** `claudia` stores more detailed, reusable sandbox configurations called "Profiles" and "Rules" in its database ([Chapter 2: Agents](02_agents_.md)). The Agent might be linked to a specific Profile, or a default Profile is used. The backend loads the rules associated with this Profile. +3. **Combine Agent Permissions and Rules:** The backend logic combines the high-level Agent toggles with the detailed Profile rules. For example, if the Agent has `enable_file_read: false`, any "file read" rules from the loaded Profile are ignored for this run. If `enable_file_read: true`, the specific paths defined in the Profile rules (like "allow reading subpaths of the project directory") are used. The project path itself (from [Chapter 1: Session/Project Management](01_session_project_management_.md)) is crucial here, as file access is often restricted to this directory. +4. **Build `gaol` Profile:** The combined set of effective rules is used to build a `gaol::profile::Profile` object in memory. This object contains the precise operations the child process will be allowed or denied. +5. **Prepare & Spawn Command:** The backend prepares the command to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). It configures the command to run within the sandbox environment defined by the `gaol` Profile. This might involve setting special environment variables or using `gaol`'s API to spawn the child process with the restrictions already applied by the parent. +6. **OS Enforces Sandbox:** When the `claude` process starts, the operating system, guided by the `gaol` library and the configured profile, actively monitors the process. If the `claude` process attempts an action that is *not* allowed by the sandbox rules (like trying to read a file outside the permitted paths when file read is enabled, or any file if file read is disabled), the operating system blocks the action immediately. +7. **Violation Logging:** If a sandboxed process attempts a forbidden action, `claudia` can detect this violation and log it to its database. This helps you understand if an Agent is trying to do something unexpected. + +Here's a simplified sequence diagram illustrating the sandboxing flow during execution: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Database as agents.db + participant SandboxLogic as Sandbox Module (Rust) + participant OS as Operating System + participant ClaudeCLI as claude binary + + Frontend->>Backend: Call execute_agent(...) + Backend->>Database: Get Agent Config (incl. permissions) + Database-->>Backend: Agent Config + Backend->>Database: Get Sandbox Profile & Rules + Database-->>Backend: Profile & Rules + Backend->>SandboxLogic: Combine Agent Permissions & Rules + SandboxLogic->>SandboxLogic: Build gaol::Profile + SandboxLogic-->>Backend: gaol::Profile ready + Backend->>OS: Spawn claude process (with gaol::Profile / env) + OS-->>Backend: Process Handle, PID + Note over OS,ClaudeCLI: OS enforces sandbox rules + ClaudeCLI->>OS: Attempt operation (e.g., read file) + alt Operation Allowed + OS-->>ClaudeCLI: Operation succeeds + else Operation Denied (Violation) + OS-->>ClaudeCLI: Operation fails (Permission denied) + Note over OS: Violation detected + OS->>SandboxLogic: Notify of violation (if configured) + SandboxLogic->>Database: Log Violation + end + ClaudeCLI-->>OS: Process exits + OS-->>Backend: Process status + Backend->>Frontend: Notify completion/output +``` + +This diagram shows how the Agent's settings propagate through the backend to influence the creation of the sandbox profile, which is then enforced by the operating system when the `claude` process is launched. + +## Diving into the Backend Code + +Let's look at snippets from the Rust code related to sandboxing, found primarily in the `src-tauri/src/sandbox/` module and `src-tauri/src/commands/sandbox.rs`. + +The `Agent` struct (from `src-tauri/src/commands/agents.rs`) holds the basic toggles: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Agent { + // ... other fields ... + pub sandbox_enabled: bool, + pub enable_file_read: bool, + pub enable_file_write: bool, // Note: This permission is often difficult to enforce precisely via sandboxing alone and might require manual user confirmation or is inherently less secure. + pub enable_network: bool, + // ... other fields ... +} +``` + +The `src-tauri/src/commands/sandbox.rs` file contains Tauri commands for managing sandbox profiles and rules stored in the database, and for viewing violations: + +```rust +// src-tauri/src/commands/sandbox.rs (Simplified) +// ... imports ... + +// Represents a detailed rule in a sandbox profile +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SandboxRule { + pub id: Option, + pub profile_id: i64, // Links to a profile + pub operation_type: String, // e.g., "file_read_all", "network_outbound" + pub pattern_type: String, // e.g., "subpath", "literal" + pub pattern_value: String, // e.g., "{{PROJECT_PATH}}", "/home/user/.config" + pub enabled: bool, + pub platform_support: Option, // e.g., "[\"macos\", \"linux\"]" + pub created_at: String, +} + +// Represents a log entry for a denied operation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SandboxViolation { + pub id: Option, + pub profile_id: Option, // What profile was active? + pub agent_id: Option, // What agent was running? + pub agent_run_id: Option, // What specific run? + pub operation_type: String, // What was attempted? + pub pattern_value: Option, // What path/address was involved? + pub process_name: Option, // Which binary? + pub pid: Option, // Which process ID? + pub denied_at: String, // When did it happen? +} + +// Tauri command to list sandbox profiles +#[tauri::command] +pub async fn list_sandbox_profiles(/* ... */) -> Result, String> { /* ... */ } + +// Tauri command to list rules for a profile +#[tauri::command] +pub async fn list_sandbox_rules(/* ... */) -> Result, String> { /* ... */ } + +// Tauri command to view recorded violations +#[tauri::command] +pub async fn list_sandbox_violations(/* ... */) -> Result, String> { /* ... */ } + +// ... other commands for creating/updating/deleting profiles and rules ... +``` + +These commands allow the frontend to manage the detailed sandbox configurations that underpin the Agent's simpler toggles. For example, when you enable "File Read Access" on an Agent, the backend loads rules of `operation_type: "file_read_all"` from the selected profile. + +The logic to combine Agent permissions, Profile rules, and build the `gaol::profile::Profile` happens in the `src-tauri/src/sandbox/profile.rs` and `src-tauri/src/sandbox/executor.rs` modules. + +The `ProfileBuilder` is used to translate `SandboxRule` database entries into `gaol::profile::Operation` objects: + +```rust +// src-tauri/src/sandbox/profile.rs (Simplified) +// ... imports ... +use gaol::profile::{Operation, PathPattern, AddressPattern, Profile}; +// ... SandboxRule struct ... + +pub struct ProfileBuilder { + project_path: PathBuf, // The current project directory + home_dir: PathBuf, // The user's home directory +} + +impl ProfileBuilder { + // ... constructor ... + + /// Build a gaol Profile from database rules, filtered by agent permissions + pub fn build_agent_profile(&self, rules: Vec, sandbox_enabled: bool, enable_file_read: bool, enable_file_write: bool, enable_network: bool) -> Result { + // If sandbox is disabled, return empty profile (no restrictions) + if !sandbox_enabled { + // ... create and return empty profile ... + } + + let mut effective_rules = Vec::new(); + + for rule in rules { + if !rule.enabled { continue; } + + // Filter rules based on Agent permissions: + let include_rule = match rule.operation_type.as_str() { + "file_read_all" | "file_read_metadata" => enable_file_read, + "network_outbound" => enable_network, + "system_info_read" => true, // System info often needed, allow if sandbox is ON + _ => true // Default to include if unknown + }; + + if include_rule { + effective_rules.push(rule); + } + } + + // Always ensure project path access is included if file read is ON + if enable_file_read { + // ... add rule for project path if not already present ... + } + + // Now build the actual gaol Profile from the effective rules + self.build_profile_with_serialization(effective_rules) // This translates rules into gaol::Operation + } + + /// Translates SandboxRules into gaol::Operation and serialized form + fn build_operation_with_serialization(&self, rule: &SandboxRule) -> Result> { + match rule.operation_type.as_str() { + "file_read_all" => { + let (pattern, path, is_subpath) = self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?; + Ok(Some((Operation::FileReadAll(pattern), SerializedOperation::FileReadAll { path, is_subpath }))) + }, + "network_outbound" => { + let (pattern, serialized) = self.build_address_pattern_with_serialization(&rule.pattern_type, &rule.pattern_value)?; + Ok(Some((Operation::NetworkOutbound(pattern), serialized))) + }, + // ... handle other operation types ... + _ => Ok(None) + } + } + + // ... helper functions to build path/address patterns ... +} +``` + +The `build_agent_profile` function is key. It takes the raw rules from the database and the Agent's simple boolean toggles, then filters the rules. It also ensures essential access (like reading the project directory) is granted if file read is enabled. Finally, it calls `build_profile_with_serialization` to create the actual `gaol::Profile` object and a simplified, serializable representation of the rules (`SerializedProfile`). + +This `SerializedProfile` is then passed to the `SandboxExecutor`: + +```rust +// src-tauri/src/sandbox/executor.rs (Simplified) +// ... imports ... +use gaol::sandbox::Sandbox; +use tokio::process::Command; +use std::path::Path; + +pub struct SandboxExecutor { + profile: gaol::profile::Profile, // The gaol profile object + project_path: PathBuf, + serialized_profile: Option, // Serialized rules for child process +} + +impl SandboxExecutor { + // ... constructor ... + + /// Prepare a tokio Command for sandboxed execution + /// The sandbox will be activated in the child process by reading environment variables + pub fn prepare_sandboxed_command(&self, command: &str, args: &[&str], cwd: &Path) -> Command { + let mut cmd = Command::new(command); + cmd.args(args).current_dir(cwd); + + // ... inherit environment variables like PATH, HOME ... + + // Serialize the sandbox rules and set environment variables + if let Some(ref serialized) = self.serialized_profile { + let rules_json = serde_json::to_string(serialized).expect("Failed to serialize rules"); + // NOTE: These environment variables are currently commented out in the actual code + // for debugging and compatibility reasons. + // In a fully enabled child-side sandboxing model, these would be set: + // cmd.env("GAOL_SANDBOX_ACTIVE", "1"); + // cmd.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref()); + // cmd.env("GAOL_SANDBOX_RULES", &rules_json); + log::warn!("🚨 Sandboxing environment variables for child process are currently disabled!"); + } else { + log::warn!("🚨 No serialized profile - running without sandbox environment!"); + } + + cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()) + } + + // ... Other execution methods ... +} +``` + +The `prepare_sandboxed_command` function takes the `gaol::profile::Profile` and the `SerializedProfile`. Although the environment variable mechanism shown above is temporarily disabled in the provided code snippets, the *intention* is for the parent process (`claudia`'s backend) to set up the environment for the child process (`claude`). The child process, if it supports this model (like `gaol`'s `ChildSandbox::activate()`), would read these environment variables upon startup and activate the sandbox *within itself* before executing the main task. + +Alternatively, `gaol` also supports launching the child process directly from the sandboxed parent using `Sandbox::start()`. The provided code attempts this first but falls back due to current `gaol` library limitations regarding getting the child process handle back. + +The `src-tauri/src/sandbox/platform.rs` file defines what kind of sandboxing capabilities are available and supported on the current operating system (Linux, macOS, FreeBSD have some support). + +```rust +// src-tauri/src/sandbox/platform.rs (Simplified) +// ... imports ... + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlatformCapabilities { + pub os: String, + pub sandboxing_supported: bool, // Is sandboxing generally possible? + // ... details about specific operation support ... +} + +pub fn get_platform_capabilities() -> PlatformCapabilities { /* ... detects OS and returns capabilities ... */ } +pub fn is_sandboxing_available() -> bool { /* ... checks if OS is supported ... */ } +``` + +This is used by the UI (via the `get_platform_capabilities` command) to inform the user if sandboxing is fully supported or if there are limitations on their platform. + +In summary, sandboxing in `claudia` works by: +1. Allowing users to set high-level permissions (read/write/network) on Agents via the UI. +2. Storing detailed, reusable sandbox Profiles and Rules in the backend database. +3. Combining Agent permissions with Profile rules in the backend to create a specific set of restrictions for a given process run. +4. Using system-level sandboxing features (via the `gaol` library and potentially environment variables) to apply these restrictions when launching the `claude` process. +5. Logging any attempts by the sandboxed process to violate these rules. + +This multi-layered approach provides both ease of use (Agent toggles) and flexibility (detailed rules in Profiles), significantly improving security when running AI-generated instructions or code. + +## Conclusion + +In this chapter, we explored **Sandboxing**, `claudia`'s security system. We learned why running external processes requires security measures and how sandboxing provides a protective barrier to limit what the `claude` process can access or do. + +We saw how you control sandboxing primarily through Agent permissions in the UI, enabling or disabling file read, file write, and network access. We then dived into the backend to understand how these simple toggles are combined with detailed Sandbox Profile rules to build a concrete `gaol::profile::Profile`. This profile is then used to launch the `claude` binary within a restricted environment enforced by the operating system, with potential violations being logged. + +Understanding sandboxing is key to securely leveraging the power of Claude Code, especially when it interacts with your local file system. + +In the next chapter, we'll learn how `claudia` handles the continuous stream of output from the `claude` binary to update the UI in real-time: [Streamed Output Processing](07_streamed_output_processing_.md). + +[Next Chapter: Streamed Output Processing](07_streamed_output_processing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/sandbox.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/executor.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/mod.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/platform.rs), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/profile.rs), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentSandboxSettings.tsx) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-7.Streamed Output Processing.md b/Claudia-docs/V_1/claudia-7.Streamed Output Processing.md new file mode 100644 index 00000000..dd0bfc9a --- /dev/null +++ b/Claudia-docs/V_1/claudia-7.Streamed Output Processing.md @@ -0,0 +1,295 @@ +# Chapter 7: Streamed Output Processing + +Welcome back to the `claudia` tutorial! In our previous chapters, we've learned about organizing your work with [Session/Project Management](01_session_project_management_.md), defining specialized assistants with [Agents](02_agents_.md), how the [Frontend UI Components](03_frontend_ui_components_.md) create the user interface, how [Tauri Commands](04_tauri_commands_.md) connect the frontend and backend, how `claudia` interacts with the `claude` command-line tool in [Claude CLI Interaction](05_claude_cli_interaction_.md), and how [Sandboxing](06_sandboxing_.md) keeps things secure. + +Now, let's look at how `claudia` handles the constant flow of information coming *from* the `claude` binary while it's running. This is the concept of **Streamed Output Processing**. + +## The Problem: Real-time Updates + +Imagine you ask Claude Code to perform a complex task, like analyzing your codebase or generating a long piece of documentation. This process can take time. The `claude` command-line tool doesn't just wait until it's completely finished and then dump all the results at once. Instead, it often sends its output piece by piece: a thought process here, a tool call there, a chunk of generated text, and finally, a result message. + +As a user of `claudia`'s graphical interface, you don't want to stare at a frozen screen waiting for everything to finish. You want to see what Claude is doing *right now*, as it's happening. You want a live view of its progress. + +This is the problem that Streamed Output Processing solves. `claudia` needs to capture this real-time, piece-by-piece output from the `claude` process and display it to you instantly. + +Think of it like watching a live news feed or a chat application. Messages appear as they are sent, not all bundled up and delivered at the very end. + +## What is Streamed Output Processing? + +Streamed Output Processing in `claudia` refers to the entire system that: + +1. **Captures** the output from the running `claude` process *as it is generated*. +2. **Receives** this output in the backend, often as a stream of data. +3. **Parses** this data (which is typically in a specific format called JSONL) line by line. +4. **Transforms** each parsed piece into a structured message that the frontend understands. +5. **Sends** these structured messages from the backend to the frontend immediately. +6. **Displays** these messages in the user interface as they arrive, providing a live, dynamic view. + +The core idea is that the output is treated as a *stream* – a continuous flow of data arriving over time – rather than a single large block of data at the end. + +## How it Looks in the UI + +When you execute an Agent or run an interactive session in `claudia`, the main part of the screen fills up with messages as they come in. + +You'll see different types of messages appear: + +* Initial system messages (showing session info, tools available). +* Assistant messages (Claude's thoughts, text, tool calls). +* User messages (your prompts, tool results sent back to Claude). +* Result messages (indicating the overall success or failure of a step). + +Each of these appears in the UI as soon as `claudia` receives the corresponding piece of output from the `claude` process. + +In the frontend code (like `src/components/AgentExecution.tsx` or `src/components/ClaudeCodeSession.tsx`), there's a state variable, typically an array, that holds all the messages displayed. When a new piece of output arrives, this array is updated, and React automatically re-renders the list to include the new message. + +For example, in `AgentExecution.tsx`, you'll find code like this managing the displayed messages: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... + +interface AgentExecutionProps { + // ... props ... +} + +export interface ClaudeStreamMessage { + type: "system" | "assistant" | "user" | "result"; + // ... other fields based on the JSONL structure ... +} + +export const AgentExecution: React.FC = ({ + agent, + onBack, + className, +}) => { + // State to hold the list of messages displayed in the UI + const [messages, setMessages] = useState([]); + // ... other state variables ... + + // ... handleExecute function ... + + // When a new message arrives (handled by an event listener, shown below): + const handleNewMessage = (newMessage: ClaudeStreamMessage) => { + setMessages(prev => [...prev, newMessage]); // Add the new message to the array + }; + + // ... render function ... + // The rendering logic maps over the `messages` array to display each one + // using the StreamMessage component + /* + return ( + // ... layout ... +
+ {messages.map((message, index) => ( + // Render each message + ))} +
+ // ... rest of component ... + ); + */ +}; +// ... rest of file ... +``` + +This state update (`setMessages`) is the frontend's way of saying, "Hey React, something new arrived, please update the list!" + +## How it Works: The Data Flow + +The communication happens in several steps, involving the `claude` binary, the operating system's pipes, the `claudia` backend (Rust), the Tauri framework, and the `claudia` frontend (TypeScript/React). + +1. **`claude` writes output:** The `claude` process executes your request. When it has a piece of output to share (like a tool call or a chunk of text), it writes it to its standard output (stdout). +2. **OS captures output:** Because `claudia`'s backend spawned `claude` with piped stdout ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)), the operating system redirects `claude`'s stdout into a temporary buffer or pipe that the `claudia` backend can read from. +3. **Backend reads line by line:** The `claudia` backend continuously reads from this pipe. It's specifically looking for newline characters to know when a complete line (a complete JSONL entry) has arrived. +4. **Backend emits event:** As soon as the backend reads a complete line, it takes the raw string data and emits it as a Tauri event. These events have a specific name (like `"agent-output"` or `"claude-output"`) that the frontend is listening for. +5. **Tauri delivers event:** The Tauri framework acts as the messenger, efficiently delivering the event and its data payload from the backend Rust process to the frontend JavaScript process. +6. **Frontend receives event:** The frontend has registered event listeners using Tauri's event API. When an event with the matching name arrives, the registered callback function is executed. +7. **Frontend processes and updates:** The callback function receives the raw output line. It parses the JSONL string into a JavaScript object and updates the component's state (`messages` array). +8. **UI re-renders:** React detects the state change and updates only the necessary parts of the UI to display the new message. + +Here is a simplified sequence diagram for this process: + +```mermaid +sequenceDiagram + participant ClaudeCLI as claude binary + participant OS as OS Pipe + participant Backend as Backend Commands (Rust) + participant Tauri as Tauri Core + participant Frontend as Frontend UI (TS/React) + + ClaudeCLI->>OS: Write line (JSONL) to stdout + OS-->>Backend: Data available in pipe + Backend->>Backend: Read line from pipe + Backend->>Tauri: Emit event "claude-output" with line data + Tauri->>Frontend: Deliver event + Frontend->>Frontend: Receive event in listener + Frontend->>Frontend: Parse JSONL line to message object + Frontend->>Frontend: Update state (add message to list) + Frontend->>Frontend: UI re-renders + Frontend->>User: Display new message in UI +``` + +This flow repeats every time `claude` outputs a new line, providing the smooth, real-time updates you see in the `claudia` interface. + +## Diving into the Code + +Let's look at the relevant code snippets from both the backend (Rust) and the frontend (TypeScript). + +### Backend: Reading and Emitting + +As seen in [Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md), the backend uses `tokio` to handle the asynchronous reading of the process's standard output. It spawns a task that reads line by line and emits events. + +Here's a simplified look at the part of `src-tauri/src/commands/claude.rs` (or similar module) that does this: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::io::{AsyncBufReadExt, BufReader}; +use tauri::{AppHandle, Manager}; +use tokio::process::Command; // Assuming command is already built + +/// Helper function to spawn Claude process and handle streaming +async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> { + // ... Configure stdout/stderr pipes ... + cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped()); + + let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?; + + let stdout = child.stdout.take().ok_or("Failed to get stdout")?; + let stdout_reader = BufReader::new(stdout); + + // Spawn a task to read stdout line by line and emit events + let app_handle_stdout = app.clone(); // Clone handle for the async task + tokio::spawn(async move { + let mut lines = stdout_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + // Log or process the raw line + log::debug!("Claude stdout line: {}", line); + // Emit the line as an event to the frontend + let _ = app_handle_stdout.emit("claude-output", &line); // <-- Emitting the event! + } + log::info!("Finished reading Claude stdout."); + }); + + // ... Similar task for stderr ... + // ... Task to wait for process exit and emit completion event ... + + Ok(()) +} + +// Example Tauri command calling the helper +/* +#[tauri::command] +pub async fn execute_claude_code(app: AppHandle, project_path: String, prompt: String, model: String) -> Result<(), String> { + // ... build the Command object 'cmd' ... + spawn_claude_process(app, cmd).await // Calls the streaming helper +} +*/ +``` + +The crucial part here is the `tokio::spawn` block that reads lines (`lines.next_line().await`) and, for each line, calls `app_handle_stdout.emit("claude-output", &line)`. This sends the raw JSONL line string to the frontend via the Tauri event system. The `"claude-output"` string is the event name. + +### Frontend: Listening and Processing + +In the frontend (TypeScript), the component that displays the output (like `AgentExecution.tsx` or `ClaudeCodeSession.tsx`) needs to set up listeners for these events when it loads and clean them up when it unmounts. + +Here's a simplified look at the event listener setup in `AgentExecution.tsx`: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +// ... ClaudeStreamMessage type ... + +export const AgentExecution: React.FC = ({ + agent, + onBack, + className, +}) => { + const [messages, setMessages] = useState([]); + const [rawJsonlOutput, setRawJsonlOutput] = useState([]); // Keep raw output too + // ... other state variables ... + + const unlistenRefs = useRef([]); // Ref to store unlisten functions + + useEffect(() => { + // Set up event listeners when the component mounts or execution starts + let outputUnlisten: UnlistenFn | undefined; + let errorUnlisten: UnlistenFn | undefined; + let completeUnlisten: UnlistenFn | undefined; + + const setupListeners = async () => { + try { + // Listen for lines from stdout + outputUnlisten = await listen("agent-output", (event) => { // <-- Listening for the event! + try { + // The event payload is the raw JSONL line string + const rawLine = event.payload; + setRawJsonlOutput(prev => [...prev, rawLine]); // Store raw line + + // Parse the JSONL string into a JavaScript object + const message = JSON.parse(rawLine) as ClaudeStreamMessage; + + // Update the messages state, triggering a UI re-render + setMessages(prev => [...prev, message]); // <-- Updating state! + + } catch (err) { + console.error("Failed to process Claude output line:", err, event.payload); + // Handle parsing errors if necessary + } + }); + + // Listen for stderr lines (errors) + errorUnlisten = await listen("agent-error", (event) => { + console.error("Claude stderr:", event.payload); + // You might want to display these errors in the UI too + }); + + // Listen for the process completion event + completeUnlisten = await listen("agent-complete", (event) => { + console.log("Claude process complete:", event.payload); + // Update UI state (e.g., hide loading indicator) + // ... update isRunning state ... + }); + + // Store unlisten functions so we can clean them up later + unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten]; + + } catch (err) { + console.error("Failed to set up event listeners:", err); + // Handle listener setup errors + } + }; + + setupListeners(); + + // Clean up listeners when the component unmounts + return () => { + unlistenRefs.current.forEach(unlisten => unlisten()); + }; + }, []); // Empty dependency array means setup runs once on mount + + // ... render function ... +}; +// ... rest of file ... +``` + +This `useEffect` hook sets up the listener using `listen("agent-output", ...)`. The callback function receives the event, accesses the raw JSONL string via `event.payload`, parses it with `JSON.parse`, and then updates the `messages` state using `setMessages`. This sequence is the core of the streamed output processing on the frontend. The `useRef` and the cleanup function in the `useEffect` are standard React patterns for managing resources (like event listeners) that persist across renders but need to be cleaned up when the component is no longer needed. + +The parsed `message` object is then passed down to the `StreamMessage` component (referenced in the provided code snippet for `src/components/StreamMessage.tsx`) which knows how to interpret the different `type` and `subtype` fields (like "assistant", "tool_use", "tool_result", "result") and render them with appropriate icons, formatting, and potentially syntax highlighting (using libraries like `react-markdown` and `react-syntax-highlighter`) or custom widgets ([ToolWidgets.tsx]). + +## Conclusion + +In this chapter, we explored **Streamed Output Processing**, understanding how `claudia` handles the real-time flow of information from the running `claude` command-line tool. We learned that `claude` sends output piece by piece in JSONL format, and that `claudia`'s backend captures this stream, reads it line by line, and immediately emits each line as a Tauri event to the frontend. + +On the frontend, we saw how components use `listen` to subscribe to these events, parse the JSONL payload into structured message objects, and update their state to display the new information dynamically. This entire process ensures that the `claudia` UI provides a responsive, live view of the AI's progress and actions during interactive sessions and Agent runs. + +Understanding streamed output is key to seeing how `claudia` provides its core real-time chat and execution experience on top of a command-line binary. + +In the next chapter, we'll look at how `claudia` keeps track of multiple potentially running processes, like Agent runs or direct sessions: [Process Registry](08_process_registry_.md). + +[Next Chapter: Process Registry](08_process_registry_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/StreamMessage.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ToolWidgets.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/types/enhanced-messages.ts) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-8.Process Registry.md b/Claudia-docs/V_1/claudia-8.Process Registry.md new file mode 100644 index 00000000..592990ce --- /dev/null +++ b/Claudia-docs/V_1/claudia-8.Process Registry.md @@ -0,0 +1,371 @@ +# Chapter 8: Process Registry + +Welcome back to the `claudia` tutorial! In our last chapter, [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md), we learned how `claudia` captures and displays the output from the `claude` command-line tool in real-time as it's running. + +Now, let's talk about something that happens just *before* that output starts streaming: launching the `claude` tool itself. When you click "Execute" for an Agent or start a new session, `claudia` doesn't just run the command and wait; it starts the `claude` binary as a separate **process** that runs in the background. + +What if you run multiple agents? What if you start a session and then switch to look at something else while it's running? How does `claudia` keep track of all these separate `claude` processes? How does it know which process is which? And how can it show you their status or let you stop them if needed? + +This is where the **Process Registry** comes in. + +## What is the Process Registry? + +Think of the Process Registry as `claudia`'s internal "Task Manager" specifically for the `claude` processes it starts. It's a system within the `claudia` backend (the Rust code) that keeps a list of all the `claude` processes that are currently running. + +For each running process, the registry stores important information, such as: + +* A unique identifier for this specific "run" (like the `run_id` we saw for Agent Runs in [Chapter 2: Agents](02_agents_.md)). +* The **Process ID (PID)** assigned by the operating system. This is like the process's unique phone number that the operating system uses to identify it. +* The current **status** (like "running", "completed", "failed", "cancelled"). +* Information about *what* is being run (like which Agent, the task description, the project path). +* A reference to the process itself, allowing `claudia` to interact with it (like sending a signal to stop it). +* A temporary buffer to hold the most recent output, allowing quick access to live status without reading the entire JSONL file every time. + +The Process Registry allows `claudia` to monitor these background processes, provide access to their live output streams (as discussed in [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)), and offer controls like stopping a running task. + +## The Use Case: Managing Running Sessions + +The most visible use case for the Process Registry in `claudia` is the "Running Sessions" screen. This screen lists all the Agent runs or interactive sessions that `claudia` has started and are still considered "active" (running or perhaps recently finished but not yet fully cleaned up). + +Here's a simplified look at the frontend component responsible for this, `RunningSessionsView.tsx`: + +```typescript +// src/components/RunningSessionsView.tsx (Simplified) +import { useState, useEffect } from 'react'; +// ... other imports ... +import { api } from '@/lib/api'; // Import API helper +import type { AgentRun } from '@/lib/api'; // Import data type + +export function RunningSessionsView({ /* ... props ... */ }) { + const [runningSessions, setRunningSessions] = useState([]); // State to hold list + const [loading, setLoading] = useState(true); + // ... other state ... + + // Function to fetch the list of running sessions + const loadRunningSessions = async () => { + try { + // Call the backend command to get running sessions + const sessions = await api.listRunningAgentSessions(); + setRunningSessions(sessions); // Update state with the list + } catch (error) { + console.error('Failed to load running sessions:', error); + // ... handle error ... + } finally { + setLoading(false); + } + }; + + // Function to stop a session + const killSession = async (runId: number, agentName: string) => { + try { + // Call the backend command to kill a session + const success = await api.killAgentSession(runId); + if (success) { + console.log(`${agentName} session stopped.`); + // Refresh the list after killing + await loadRunningSessions(); + } else { + console.warn('Session may have already finished'); + } + } catch (error) { + console.error('Failed to kill session:', error); + // ... handle error ... + } + }; + + useEffect(() => { + loadRunningSessions(); // Load sessions when component mounts + + // Set up auto-refresh + const interval = setInterval(() => { + loadRunningSessions(); + }, 5000); // Refresh every 5 seconds + + return () => clearInterval(interval); // Clean up interval + }, []); + + if (loading) { + return

Loading running sessions...

; // Loading indicator + } + + return ( +
+

Running Agent Sessions

+ {runningSessions.length === 0 ? ( +

No agent sessions are currently running

+ ) : ( +
+ {/* Map over the runningSessions list to display each one */} + {runningSessions.map((session) => ( +
{/* Card or similar display */} +

{session.agent_name}

+

Status: {session.status}

+

PID: {session.pid}

+ {/* ... other details like task, project path, duration ... */} + + {/* Buttons to interact with the session */} + {/* Set state to open viewer */} + +
+ ))} +
+ )} + + {/* Session Output Viewer component (shown when selectedSession is not null) */} + {selectedSession && ( + setSelectedSession(null)} + /> + )} +
+ ); +} +``` + +This component demonstrates how the frontend relies on the backend's Process Registry: +1. It calls `api.listRunningAgentSessions()` to get the current list. +2. It displays information for each running process, including the PID and status. +3. It provides "Stop" buttons that call `api.killAgentSession(runId)`, requesting the backend to terminate the corresponding process. +4. It provides a "View Output" button that, when clicked, might fetch the live output buffer from the registry (using a command like `api.getLiveSessionOutput(runId)`) before potentially switching to file-based streaming ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)). +5. It automatically refreshes this list periodically by calling `loadRunningSessions` again. + +## How it Works: Under the Hood + +The Process Registry is implemented in the Rust backend, primarily in the `src-tauri/src/process/registry.rs` file. + +Here's a simplified look at what happens step-by-step: + +1. **Process Spawned:** When a backend command like `execute_agent` or `execute_claude_code` needs to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)), it prepares the command and then calls `child.spawn()`. +2. **Registration:** Immediately after `child.spawn()` successfully starts the process, the backend extracts the **PID** from the returned `Child` object. It then takes the `run_id` (generated when the Agent run record was created in the database), the PID, and other relevant info (Agent name, task, project path) and calls a method on the `ProcessRegistry` instance, typically `registry.register_process(...)`. +3. **Registry Storage:** The `ProcessRegistry` stores this information in an in-memory data structure, like a `HashMap`, where the key is the `run_id` and the value is an object containing the `ProcessInfo` and the actual `Child` handle. It also initializes a buffer for live output for this specific run. +4. **Output Appending:** As the streaming output processing ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)) reads lines from the process's stdout/stderr pipes, it also appends these lines to the live output buffer associated with this run_id in the Process Registry using `registry.append_live_output(run_id, line)`. +5. **Listing Processes:** When the frontend calls `list_running_agent_sessions` (which maps to a backend command like `list_running_sessions`), the backend accesses the `ProcessRegistry` and asks it for the list of currently registered processes (`registry.get_running_processes()`). The registry returns the stored `ProcessInfo` for each active entry in its map. +6. **Viewing Live Output:** When the frontend calls `get_live_session_output(runId)`, the backend asks the registry for the live output buffer associated with that `runId` (`registry.get_live_output(runId)`), and returns it to the frontend. +7. **Killing Process:** When the frontend calls `kill_agent_session(runId)`, the backend first tells the `ProcessRegistry` to attempt to terminate the process (`registry.kill_process(runId)`). The registry uses the stored `Child` handle or PID to send a termination signal to the operating system. After attempting the kill, the backend also updates the database record for that run to mark its status as 'cancelled'. +8. **Cleanup:** Periodically, `claudia` runs a cleanup task (`cleanup_finished_processes`) that checks the status of processes currently in the registry. If a process has exited (e.g., finished naturally or was killed), the registry removes its entry (`registry.unregister_process(runId)`). This also helps keep the database status accurate. + +Here's a simple sequence diagram showing the core interactions: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI + participant Backend as Backend Commands + participant Registry as Process Registry + participant OS as Operating System + + User->>Frontend: Open Running Sessions View + Frontend->>Backend: Call list_running_sessions() + Backend->>Registry: get_running_processes() + Registry-->>Backend: Return List + Backend-->>Frontend: Return List (mapped from ProcessInfo) + Frontend->>User: Display List + + User->>Frontend: Click Stop Button (for runId) + Frontend->>Backend: Call kill_agent_session(runId) + Backend->>Registry: kill_process(runId) + Registry->>OS: Send terminate signal (using PID/Handle) + OS-->>Registry: Confirmation/Status + Registry-->>Backend: Return success/failure + Backend->>Backend: Update AgentRun status in DB + Backend-->>Frontend: Return confirmation + Frontend->>Frontend: Refresh list / Update UI +``` + +This diagram illustrates how the frontend relies on backend commands to query and manage the processes tracked by the Process Registry. + +## Diving into the Backend Code + +The core implementation of the Process Registry is found in `src-tauri/src/process/registry.rs`. + +First, let's look at the `ProcessInfo` struct, which holds the basic details about a running process: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... imports ... +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// Information about a running agent process +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcessInfo { + pub run_id: i64, // Matches the agent_runs database ID + pub agent_id: i64, // Which agent started this run + pub agent_name: String, // Agent's name + pub pid: u32, // Operating System Process ID + pub started_at: DateTime, // When it started + pub project_path: String, // Where it's running + pub task: String, // The task given + pub model: String, // The model used +} +``` + +The `ProcessRegistry` struct itself is simple; it just holds the map and uses `Arc>` for thread-safe access because multiple parts of the backend might need to interact with it concurrently. + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... imports ... +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use tokio::process::Child; // Need the process handle itself + +/// Information about a running process with handle +pub struct ProcessHandle { + pub info: ProcessInfo, + pub child: Arc>>, // The handle to the child process + pub live_output: Arc>, // Buffer for live output +} + +/// Registry for tracking active agent processes +pub struct ProcessRegistry { + // Map from run_id to the ProcessHandle + processes: Arc>>, +} + +impl ProcessRegistry { + pub fn new() -> Self { + Self { + processes: Arc::new(Mutex::new(HashMap::new())), + } + } + + // ... methods like register_process, unregister_process, get_running_processes, kill_process, append_live_output, get_live_output ... +} + +// Tauri State wrapper for the registry +pub struct ProcessRegistryState(pub Arc); +// ... Default impl ... +``` + +When a process is spawned, the `execute_agent` command (in `src-tauri/src/commands/agents.rs`) calls `registry.register_process`: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +// ... imports ... +// Assuming 'registry' is the State +// Assuming 'child' is the tokio::process::Child from cmd.spawn()... +// Assuming 'run_id', 'agent_id', etc., are defined... + +// Register the process in the registry +registry.0.register_process( + run_id, + agent_id, + agent.name.clone(), // Agent name + pid, // Process ID + project_path.clone(), + task.clone(), + execution_model.clone(), + child, // Pass the child handle +).map_err(|e| format!("Failed to register process: {}", e))?; + +info!("📋 Registered process in registry"); + +// ... rest of the async task waiting for process to finish ... +``` + +The `register_process` method in the `ProcessRegistry` then locks the internal map and inserts the new entry: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... + +/// Register a new running process +pub fn register_process( + &self, + run_id: i64, + agent_id: i64, + agent_name: String, + pid: u32, + project_path: String, + task: String, + model: String, + child: Child, // Receives the child handle +) -> Result<(), String> { + let mut processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + + let process_info = ProcessInfo { + run_id, agent_id, agent_name, pid, + started_at: Utc::now(), + project_path, task, model, + }; + + let process_handle = ProcessHandle { + info: process_info, + child: Arc::new(Mutex::new(Some(child))), // Store the handle + live_output: Arc::new(Mutex::new(String::new())), // Init output buffer + }; + + processes.insert(run_id, process_handle); // Insert into the map + Ok(()) +} +``` + +Listing running processes involves locking the map and collecting the `ProcessInfo` from each `ProcessHandle`: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... + +/// Get all running processes +pub fn get_running_processes(&self) -> Result, String> { + let processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + // Iterate through the map's values (ProcessHandle), clone the info field, collect into a Vec + Ok(processes.values().map(|handle| handle.info.clone()).collect()) +} +``` + +Killing a process involves looking up the `ProcessHandle` by `run_id`, accessing the stored `Child` handle, and calling its `kill` method: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... +use tokio::process::Child; + +/// Kill a running process +pub async fn kill_process(&self, run_id: i64) -> Result { + let processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + + if let Some(handle) = processes.get(&run_id) { + let child_arc = handle.child.clone(); + drop(processes); // IMPORTANT: Release the lock before calling async kill() + + let mut child_guard = child_arc.lock().map_err(|e| e.to_string())?; // Lock the child handle + if let Some(ref mut child) = child_guard.as_mut() { + match child.kill().await { // Call the async kill method + Ok(_) => { + *child_guard = None; // Clear the handle after killing + Ok(true) + } + Err(e) => Err(format!("Failed to kill process: {}", e)), + } + } else { + Ok(false) // Process was already killed or completed + } + } else { + Ok(false) // Process not found in registry + } +} +``` + +Note that the `kill_agent_session` Tauri command ([src-tauri/src/commands/agents.rs]) first calls `registry.kill_process` to try terminating the *actual* OS process via the `Child` handle, and *then* updates the database status. This ensures the UI accurately reflects the state even if the process doesn't immediately exit after the signal. + +The `cleanup_finished_processes` command (also in `src-tauri/src/commands/agents.rs`) periodically checks all processes currently in the registry using `registry.is_process_running()` and, if they are no longer running, updates their status in the database and removes them from the registry. + +This Process Registry provides the backend's central point for managing and interacting with all the separate `claude` instances that `claudia` is running, enabling features like the "Running Sessions" view and the ability to stop tasks. + +## Conclusion + +In this chapter, we introduced the **Process Registry**, `claudia`'s internal system for tracking the `claude` command-line tool processes it launches in the background. We learned that it stores essential information like PID, status, and associated run details, allowing `claudia` to monitor and control these separate tasks. + +We saw how the Process Registry is used to power features like the "Running Sessions" view in the UI, enabling users to see what's currently executing, view live output, and stop processes. We also delved into the backend implementation, seeing how processes are registered upon spawning, how the registry stores their handles, and how backend commands interact with the registry to list, kill, and manage these running tasks. + +Understanding the Process Registry is key to seeing how `claudia` manages concurrency and provides visibility and control over the AI tasks running on your system. + +In the next chapter, we'll explore **Checkpointing**, a feature that allows Claude Code to save and restore its state, enabling longer, more complex interactions across multiple runs. + +[Next Chapter: Checkpointing](09_checkpointing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/process/mod.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/process/registry.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/RunningSessionsView.tsx) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-9.Checkpoiting.md b/Claudia-docs/V_1/claudia-9.Checkpoiting.md new file mode 100644 index 00000000..51eb81f1 --- /dev/null +++ b/Claudia-docs/V_1/claudia-9.Checkpoiting.md @@ -0,0 +1,935 @@ +# Chapter 9: Checkpointing + +Welcome back to the `claudia` tutorial! In our previous chapter, [Chapter 8: Process Registry](08_process_registry_.md), we learned how `claudia` keeps track of and manages the individual `claude` processes it launches. This allows the application to handle multiple running tasks simultaneously and provides a view of what's currently active. + +Now, let's talk about preserving the state of those tasks over time, even after they finish or the application closes. This is the powerful concept of **Checkpointing**. + +## The Problem: Sessions Are Temporary + +Imagine you're working with Claude Code on a complex feature development within a project. You have a long conversation, make several changes to files, get some code snippets, debug an issue, and maybe even use tools to run tests. This interaction might span hours or even days across multiple `claude` runs. + +Each run of `claude` is a session ([Chapter 1: Session/Project Management](01_session_project_management_.md)), and the CLI automatically saves the message history for that session. But what about the state of your project files? What if you want to go back to how the files looked *before* Claude made a specific set of changes? What if you want to experiment with a different approach, but keep the option to return to the current state? + +The basic session history saves the *conversation*, but it doesn't version control your *project files*. This is where checkpoints become essential. + +Think of it like writing a book. The message history is like your rough draft – a linear flow of words. But sometimes you want to save a specific version (e.g., "finished Chapter 5"), experiment with rewriting a scene, and maybe decide later to revert to that saved version or start a new version branched from it. Checkpointing provides this capability for your AI-assisted coding sessions. + +## What is Checkpointing? + +Checkpointing in `claudia` is a system for creating save points of your entire working state for a specific Claude Code session. A checkpoint captures two main things at a particular moment: + +1. **The complete message history** up to that point in the session. +2. **Snapshots of your project files** that have changed since the last checkpoint (or are being tracked). + +When you create a checkpoint, `claudia` records the session's conversation history and saves copies of the relevant files in a special location. This lets you revisit that exact moment later. + +**In simpler terms:** + +* A Checkpoint is a snapshot of your conversation *and* your project files at a specific point in time. +* You can create checkpoints manually whenever you want to save a significant state (like "After implementing Login feature"). +* `claudia` can also create checkpoints automatically based on certain events (like after a tool makes changes to files). +* Checkpoints are organized in a **Timeline**, showing the history of your session like a branching tree (similar to how git commits work). +* You can **Restore** a checkpoint to revert your message history and project files to that saved state. +* You can **Fork** from a checkpoint to start a new conversation branch from a previous state. +* You can **Diff** between checkpoints to see exactly which files were changed and what the changes were. + +## Key Concepts in Checkpointing + +Let's break down the core ideas behind `claudia`'s checkpointing system: + +| Concept | Description | Analogy | +| :----------------- | :--------------------------------------------------------------------------------------------------------- | :---------------------------------------------- | +| **Checkpoint** | A specific save point containing session messages and file snapshots. | Saving your game progress. | +| **Timeline** | The chronological history of checkpoints for a session, shown as a tree structure reflecting branching (forks). | A Git history tree or a family tree. | +| **File Snapshot** | A saved copy of a project file's content and metadata at a specific checkpoint. Only saves changes efficiently. | Saving individual changed files in a commit. | +| **Restoring** | Reverting the current session messages and project files to the state captured in a chosen checkpoint. | Loading a previous save game. | +| **Forking** | Creating a new session branch starting from a specific checkpoint. | Branching in Git or creating an alternate story. | +| **Automatic Checkpoints** | Checkpoints created by `claudia` based on predefined rules (e.g., after certain actions). | Auto-save feature in software. | +| **Checkpoint Strategy** | The specific rule defining when automatic checkpoints are created (Per Prompt, Per Tool Use, Smart). | Different auto-save frequencies/triggers. | +| **Diffing** | Comparing two checkpoints to see the differences in file content and token usage. | `git diff` command. | + +## Using Checkpointing in the UI + +You interact with checkpointing primarily within a specific session view (like `ClaudeCodeSession.tsx`), typically via a dedicated section or side panel. + +The `TimelineNavigator.tsx` component is the central piece of the UI for browsing and interacting with checkpoints: + +```typescript +// src/components/TimelineNavigator.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { GitBranch, Save, RotateCcw, GitFork, Diff } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { api, type Checkpoint, type TimelineNode, type SessionTimeline, type CheckpointDiff } from "@/lib/api"; // Import types and API + +// ... component props interface ... + +/** + * Visual timeline navigator for checkpoint management + */ +export const TimelineNavigator: React.FC = ({ + sessionId, + projectId, + projectPath, + currentMessageIndex, + onCheckpointSelect, // Callback for selecting a checkpoint (e.g., for Diff) + onFork, // Callback for triggering a fork + refreshVersion = 0, // Prop to force reload + className +}) => { + const [timeline, setTimeline] = useState(null); // State for the timeline data + const [selectedCheckpoint, setSelectedCheckpoint] = useState(null); // State for the currently selected checkpoint (for diffing, etc.) + const [showCreateDialog, setShowCreateDialog] = useState(false); // State for the "Create Checkpoint" dialog + const [checkpointDescription, setCheckpointDescription] = useState(""); // State for the description input + const [isLoading, setIsLoading] = useState(false); + // ... other state for diff dialog, errors, etc. ... + + // Effect to load the timeline when the component mounts or needs refreshing + useEffect(() => { + loadTimeline(); + }, [sessionId, projectId, projectPath, refreshVersion]); // Dependencies + + // Function to load timeline data from backend + const loadTimeline = async () => { + try { + setIsLoading(true); + // Call backend API to get the timeline + const timelineData = await api.getSessionTimeline(sessionId, projectId, projectPath); + setTimeline(timelineData); // Update state + // ... logic to auto-expand current branch ... + } catch (err) { + console.error("Failed to load timeline:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle manual checkpoint creation + const handleCreateCheckpoint = async () => { + try { + setIsLoading(true); + // Call backend API to create a checkpoint + await api.createCheckpoint( + sessionId, + projectId, + projectPath, + currentMessageIndex, // Pass current message count + checkpointDescription || undefined // Pass optional description + ); + setCheckpointDescription(""); // Clear input + setShowCreateDialog(false); // Close dialog + await loadTimeline(); // Reload timeline to show the new checkpoint + } catch (err) { + console.error("Failed to create checkpoint:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle restoring a checkpoint + const handleRestoreCheckpoint = async (checkpoint: Checkpoint) => { + // ... confirmation logic ... + try { + setIsLoading(true); + // Call backend API to restore the checkpoint + await api.restoreCheckpoint(checkpoint.id, sessionId, projectId, projectPath); + await loadTimeline(); // Reload timeline + // Notify parent component or session view about the restore + // This might trigger reloading the message history from the checkpoint + onCheckpointSelect(checkpoint); + } catch (err) { + console.error("Failed to restore checkpoint:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle forking (delegates to parent component via callback) + const handleFork = async (checkpoint: Checkpoint) => { + // This component doesn't *create* the new session, it tells the parent + // session view to initiate a fork from this checkpoint ID + onFork(checkpoint.id); + }; + + // Function to handle comparing checkpoints + const handleCompare = async (checkpoint: Checkpoint) => { + if (!selectedCheckpoint) { + // If no checkpoint is selected for comparison, select this one + setSelectedCheckpoint(checkpoint); + // You might update UI to show this checkpoint is selected for compare + return; + } + // If a checkpoint is already selected, perform the comparison + try { + setIsLoading(true); + const diffData = await api.getCheckpointDiff( + selectedCheckpoint.id, // The first selected checkpoint + checkpoint.id, // The checkpoint being compared against + sessionId, projectId // Session/Project context + ); + // ... show diffData in a dialog ... + setDiff(diffData); + // ... open diff dialog ... + } catch (err) { + console.error("Failed to get diff:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + + // Recursive function to render the timeline tree structure + const renderTimelineNode = (node: TimelineNode, depth: number = 0) => { + // ... rendering logic for node, its children, and buttons ... + // Each node displays checkpoint info and buttons for Restore, Fork, Diff + const isCurrent = timeline?.currentCheckpointId === node.checkpoint.id; + const isSelected = selectedCheckpoint?.id === node.checkpoint.id; // For compare selection + + + return ( +
+ {/* UI representation of the checkpoint */} + setSelectedCheckpoint(node.checkpoint)} // Select for compare/info + > + + {/* Display checkpoint ID, timestamp, description, metadata (tokens, files) */} +

{node.checkpoint.id.slice(0, 8)}...

+

{node.checkpoint.timestamp}

+

{node.checkpoint.description}

+ {node.checkpoint.metadata.totalTokens} tokens + {node.checkpoint.metadata.fileChanges} files changed + + {/* Action Buttons */} + + + +
+
+ + {/* Recursively render children */} + {/* ... Conditional rendering based on expanded state ... */} +
+ {node.children.map((child) => renderTimelineNode(child, depth + 1))} +
+
+ ); + }; + + return ( +
+ {/* ... Warning message ... */} + {/* Header with "Checkpoint" button */} +
+
+ +

Timeline

+ {/* Display total checkpoints badge */} +
+ +
+ + {/* Error display */} + {/* ... */} + + {/* Render the timeline tree starting from the root node */} + {timeline?.rootNode ? ( +
+ {renderTimelineNode(timeline.rootNode)} +
+ ) : ( + // ... Loading/empty state ... + )} + + {/* Create checkpoint dialog */} + + + + Create Checkpoint + {/* ... Dialog description and input for description ... */} + +
+
+ + setCheckpointDescription(e.target.value)} /> +
+
+ + {/* ... Cancel and Create buttons calling handleCreateCheckpoint ... */} + +
+
+ + {/* Diff dialog (not shown here, but would display diff state) */} + {/* ... Dialog for showing diff results ... */} +
+ ); +}; +``` + +This component displays the timeline tree structure, fetched from the backend using `api.getSessionTimeline`. Each node in the tree represents a checkpoint (`TimelineNode` contains a `Checkpoint` struct). The component provides buttons to trigger actions like creating a manual checkpoint (`handleCreateCheckpoint`), restoring a checkpoint (`handleRestoreCheckpoint`), forking (`handleFork`), and comparing checkpoints (`handleCompare`). These actions call corresponding backend API functions via `src/lib/api.ts`. + +You can also configure automatic checkpointing and cleanup using the `CheckpointSettings.tsx` component: + +```typescript +// src/components/CheckpointSettings.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { Settings, Save, Trash2, HardDrive } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { SelectComponent } from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { api, type CheckpointStrategy } from "@/lib/api"; // Import types and API + +// ... component props interface ... + +/** + * CheckpointSettings component for managing checkpoint configuration + */ +export const CheckpointSettings: React.FC = ({ + sessionId, + projectId, + projectPath, + onClose, + className, +}) => { + const [autoCheckpointEnabled, setAutoCheckpointEnabled] = useState(true); + const [checkpointStrategy, setCheckpointStrategy] = useState("smart"); + const [totalCheckpoints, setTotalCheckpoints] = useState(0); + const [keepCount, setKeepCount] = useState(10); // State for cleanup setting + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + // ... error/success states ... + + const strategyOptions: SelectOption[] = [ + { value: "manual", label: "Manual Only" }, + { value: "per_prompt", label: "After Each Prompt" }, + { value: "per_tool_use", label: "After Tool Use" }, + { value: "smart", label: "Smart (Recommended)" }, + ]; + + // Load settings when component mounts + useEffect(() => { + loadSettings(); + }, [sessionId, projectId, projectPath]); + + const loadSettings = async () => { + try { + setIsLoading(true); + // Call backend API to get settings + const settings = await api.getCheckpointSettings(sessionId, projectId, projectPath); + setAutoCheckpointEnabled(settings.auto_checkpoint_enabled); + setCheckpointStrategy(settings.checkpoint_strategy); + setTotalCheckpoints(settings.total_checkpoints); // Get total count for cleanup info + } catch (err) { + console.error("Failed to load checkpoint settings:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + const handleSaveSettings = async () => { + try { + setIsSaving(true); + // Call backend API to update settings + await api.updateCheckpointSettings( + sessionId, + projectId, + projectPath, + autoCheckpointEnabled, + checkpointStrategy + ); + // ... show success message ... + } catch (err) { + console.error("Failed to save checkpoint settings:", err); + // ... set error state ... + } finally { + setIsSaving(false); + } + }; + + const handleCleanup = async () => { + // ... confirmation ... + try { + setIsLoading(true); + // Call backend API to cleanup + const removed = await api.cleanupOldCheckpoints( + sessionId, + projectId, + projectPath, + keepCount // Pass how many recent checkpoints to keep + ); + // ... show success message ... + await loadSettings(); // Refresh count + } catch (err) { + console.error("Failed to cleanup checkpoints:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* ... Experimental Warning ... */} + {/* Header */} +
+ {/* ... Title and icon ... */} + {onClose && } +
+ + {/* Error/Success messages */} + {/* ... */} + +
+ {/* Auto-checkpoint toggle */} +
+
+ +

Automatically create checkpoints

+
+ +
+ + {/* Checkpoint strategy select */} +
+ + setCheckpointStrategy(value as CheckpointStrategy)} + options={strategyOptions} + disabled={isLoading || !autoCheckpointEnabled} // Disable if auto-checkpoint is off + /> + {/* ... Strategy description text ... */} +
+ + {/* Save button */} + +
+ + {/* Storage Management Section */} +
+
+ {/* ... "Storage Management" title and icon ... */} +

Total checkpoints: {totalCheckpoints}

{/* Display count */} +
+ {/* Cleanup settings */} +
+ +
+ setKeepCount(parseInt(e.target.value) || 10)} disabled={isLoading} className="flex-1"/> + +
+ {/* ... Cleanup description text ... */} +
+
+ + ); +}; +``` + +This component allows you to toggle automatic checkpoints, select a strategy (Manual, Per Prompt, Per Tool Use, Smart), set how many recent checkpoints to keep, and trigger a cleanup. These actions are handled by backend commands called via `api`. + +## How it Works: Under the Hood (Backend) + +The checkpointing logic resides in the `src-tauri/src/checkpoint/` module. This module contains several key parts: + +1. **`checkpoint::mod.rs`**: Defines the main data structures (`Checkpoint`, `FileSnapshot`, `SessionTimeline`, `TimelineNode`, `CheckpointStrategy`, etc.) and utility structs (`CheckpointPaths`, `CheckpointDiff`). +2. **`checkpoint::storage.rs`**: Handles reading from and writing to disk. It manages saving/loading checkpoint metadata, messages, and file snapshots. It uses content-addressable storage for file contents to save space. +3. **`checkpoint::manager.rs`**: The core logic for managing a *single session*'s checkpoints. It tracks file changes (`FileTracker`), keeps the current message history (`current_messages`), interacts with `CheckpointStorage` for saving/loading, manages the session's `Timeline`, and handles operations like creating, restoring, and forking. +4. **`checkpoint::state.rs`**: A stateful manager (similar to the Process Registry) that holds `CheckpointManager` instances for *all active sessions* in memory. This prevents needing to recreate managers for each command call. + +Checkpoint data is stored within the `~/.claude` directory, specifically within the project's timeline directory: + +`~/.claude/projects//.timelines//` + +Inside this session timeline directory, you'll find: +* `timeline.json`: Stores the `SessionTimeline` structure (the tree metadata). +* `checkpoints/`: A directory containing subdirectories for each checkpoint ID. Each checkpoint directory (`checkpoints//`) holds `metadata.json` and `messages.jsonl` (the compressed messages). +* `files/`: A directory containing file snapshots, organized into a `content_pool/` (actual compressed file contents, stored by hash) and `refs/` (references from each checkpoint back to the content pool, stored as small JSON files). + +### The `CheckpointState` + +Just like the Process Registry manages active processes, the `CheckpointState` manages active `CheckpointManager` instances. When a session starts or is loaded in the UI, the frontend calls a backend command which then uses `CheckpointState::get_or_create_manager` to get the manager for that session. + +```rust +// src-tauri/src/checkpoint/state.rs (Simplified) +// ... imports ... +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; // For thread-safe async access + +use super::manager::CheckpointManager; + +/// Manages checkpoint managers for active sessions +#[derive(Default, Clone)] +pub struct CheckpointState { + /// Map of session_id to CheckpointManager + managers: Arc>>>, // Use RwLock for concurrent reads/writes + claude_dir: Arc>>, // Claude dir path needed for storage initialization +} + +impl CheckpointState { + // ... new(), set_claude_dir(), remove_manager(), clear_all() methods ... + + /// Gets or creates a CheckpointManager for a session + pub async fn get_or_create_manager( + &self, + session_id: String, + project_id: String, + project_path: PathBuf, + ) -> Result> { + let mut managers = self.managers.write().await; // Lock for writing + + // Check if manager already exists + if let Some(manager) = managers.get(&session_id) { + return Ok(Arc::clone(manager)); // Return existing manager (Arc::clone increases ref count) + } + + // ... get claude_dir ... + + // Create new manager if it doesn't exist + let manager = CheckpointManager::new( + project_id, + session_id.clone(), + project_path, + claude_dir, + ).await?; // CheckpointManager::new handles loading/init storage + + let manager_arc = Arc::new(manager); + managers.insert(session_id, Arc::clone(&manager_arc)); // Store new manager + + Ok(manager_arc) + } + + // ... get_manager(), list_active_sessions() methods ... +} +``` + +This structure ensures that the heavy work of loading the timeline and setting up file tracking only happens once per session when it's first accessed, not for every single checkpoint-related command. + +### Creating a Checkpoint Flow + +When the frontend requests to create a checkpoint (manually or automatically), the backend command retrieves the session's `CheckpointManager` from the `CheckpointState` and calls `manager.create_checkpoint(...)`. + +Here's a simplified look at what happens inside `CheckpointManager::create_checkpoint`: + +```rust +// src-tauri/src/checkpoint/manager.rs (Simplified) +// ... imports ... + +impl CheckpointManager { + // ... new(), track_message(), track_file_modification(), etc. ... + + /// Create a checkpoint + pub async fn create_checkpoint( + &self, + description: Option, + parent_checkpoint_id: Option, // Optional parent ID for explicit forks + ) -> Result { + let messages = self.current_messages.read().await; // Get current messages + let message_index = messages.len().saturating_sub(1); + + // ... Extract metadata (prompt, tokens, etc.) from messages ... + + // Ensure all files in the project are tracked before snapshotting + // This discovers new files and adds them to the file tracker + let mut all_files = Vec::new(); + let _ = collect_files(&self.project_path, &self.project_path, &mut all_files); + for rel in all_files { + if let Some(p) = rel.to_str() { + let _ = self.track_file_modification(p).await; // Adds/updates tracker state + } + } + + // Generate a unique ID for the new checkpoint + let checkpoint_id = storage::CheckpointStorage::generate_checkpoint_id(); + + // Create file snapshots based on the *current* state of tracked files + // This reads the content of files marked as modified by track_file_modification + let file_snapshots = self.create_file_snapshots(&checkpoint_id).await?; + + // Build the Checkpoint metadata struct + let checkpoint = Checkpoint { + id: checkpoint_id.clone(), + session_id: self.session_id.clone(), + project_id: self.project_id.clone(), + message_index, + timestamp: Utc::now(), + description, + parent_checkpoint_id: parent_checkpoint_id.or_else(|| self.timeline.read().await.current_checkpoint_id.clone()), // Link to current parent or explicit parent + // ... include extracted metadata ... + }; + + // Save the checkpoint using the storage layer + let messages_content = messages.join("\n"); + let result = self.storage.save_checkpoint( + &self.project_id, + &self.session_id, + &checkpoint, + file_snapshots, // Pass the actual snapshots + &messages_content, // Pass the message content + )?; + + // ... Reload timeline from disk to incorporate new node ... + // ... Update current_checkpoint_id in in-memory timeline ... + // ... Reset is_modified flag in the file tracker ... + + Ok(result) + } + + // Helper to create FileSnapshots from the FileTracker state + async fn create_file_snapshots(&self, checkpoint_id: &str) -> Result> { + let tracker = self.file_tracker.read().await; + let mut snapshots = Vec::new(); + + for (rel_path, state) in &tracker.tracked_files { + // Only snapshot files marked as modified or deleted + if !state.is_modified && state.exists { // Only include if modified OR was deleted + continue; // Skip if not modified AND still exists + } + if state.is_modified || !state.exists { // Snapshot if modified or is now deleted + // ... read file content, calculate hash, get metadata ... + let (content, exists, permissions, size, current_hash) = { /* ... */ }; + + snapshots.push(FileSnapshot { + checkpoint_id: checkpoint_id.to_string(), + file_path: rel_path.clone(), + content, // Content will be empty for deleted files + hash: current_hash, // Hash will be empty for deleted files + is_deleted: !exists, + permissions, + size, + }); + } + } + Ok(snapshots) + } + + // ... other methods ... +} +``` + +The `create_checkpoint` function coordinates the process: it reads current messages, identifies changed files using the `FileTracker`, generates file snapshots by reading changed file contents, creates the checkpoint metadata, saves everything to disk via `CheckpointStorage`, and updates the timeline. + +The `FileTracker` keeps a list of files that have been referenced (either by the user or by tool outputs). The `track_file_modification` method is called whenever a file might have changed (e.g., mentioned in an edit tool output). It checks the file's current state (existence, hash, modification time) and marks it as `is_modified` if it differs from the last known state. + +The `CheckpointStorage::save_checkpoint` method handles the actual disk writing, including compressing messages and file contents and managing the content-addressable storage for file snapshots (`save_file_snapshot`). + +```rust +// src-tauri/src/checkpoint/storage.rs (Simplified) +// ... imports ... + +impl CheckpointStorage { + // ... new(), init_storage(), load_checkpoint(), etc. ... + + /// Save a checkpoint to disk + pub fn save_checkpoint(/* ... arguments ... */) -> Result { + // ... create directories ... + // ... save metadata.json ... + // ... save compressed messages.jsonl ... + + // Save file snapshots (calling save_file_snapshot for each) + let mut files_processed = 0; + for snapshot in &file_snapshots { + if self.save_file_snapshot(&paths, snapshot).is_ok() { // Calls helper + files_processed += 1; + } + } + + // Update timeline file on disk + self.update_timeline_with_checkpoint(/* ... */)?; + + // ... return result ... + Ok(CheckpointResult { /* ... */ }) + } + + /// Save a single file snapshot using content-addressable storage + fn save_file_snapshot(&self, paths: &CheckpointPaths, snapshot: &FileSnapshot) -> Result<()> { + // Directory where actual file content is stored by hash + let content_pool_dir = paths.files_dir.join("content_pool"); + fs::create_dir_all(&content_pool_dir)?; + + // Path to the content file based on its hash + let content_file = content_pool_dir.join(&snapshot.hash); + + // Only write content if the file doesn't exist (avoids duplicates) + if !content_file.exists() && !snapshot.is_deleted { + // Compress and save file content + let compressed_content = encode_all(snapshot.content.as_bytes(), self.compression_level) + .context("Failed to compress file content")?; + fs::write(&content_file, compressed_content)?; + } + + // Create a reference file for this checkpoint's view of the file + let checkpoint_refs_dir = paths.files_dir.join("refs").join(&snapshot.checkpoint_id); + fs::create_dir_all(&checkpoint_refs_dir)?; + + // Save a small JSON file containing metadata and a pointer (hash) to the content pool + let ref_metadata = serde_json::json!({ + "path": snapshot.file_path, + "hash": snapshot.hash, + "is_deleted": snapshot.is_deleted, + "permissions": snapshot.permissions, + "size": snapshot.size, + }); + let safe_filename = snapshot.file_path.to_string_lossy().replace('/', "_").replace('\\', "_"); + let ref_path = checkpoint_refs_dir.join(format!("{}.json", safe_filename)); + fs::write(&ref_path, serde_json::to_string_pretty(&ref_metadata)?)?; + + Ok(()) + } + + // ... update_timeline_with_checkpoint() and other methods ... +} +``` + +This snippet shows how `save_file_snapshot` stores the *actual* file content in a `content_pool` directory, named by the file's hash. This means if the same file content appears in multiple checkpoints, it's only stored once on disk. Then, in a `refs` directory specific to the checkpoint, a small file is saved that just contains the file's metadata and a pointer (the hash) back to the content pool. + +Here is a simplified sequence diagram for creating a manual checkpoint: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI (TimelineNavigator.tsx) + participant Backend as Backend Commands (claude.rs) + participant CheckpointState as CheckpointState (state.rs) + participant CheckpointManager as CheckpointManager (manager.rs) + participant CheckpointStorage as CheckpointStorage (storage.rs) + participant Filesystem as Filesystem + + User->>Frontend: Clicks "Checkpoint" button + Frontend->>Backend: Call create_checkpoint(...) + Backend->>CheckpointState: get_or_create_manager(session_id, ...) + CheckpointState->>CheckpointState: Look up manager in map + alt Manager exists + CheckpointState-->>Backend: Return existing manager + else Manager does not exist + CheckpointState->>CheckpointManager: Create new Manager() + CheckpointManager->>CheckpointStorage: init_storage(...) + CheckpointStorage->>Filesystem: Create directories, load timeline.json + Filesystem-->>CheckpointStorage: Return timeline data / Success + CheckpointStorage-->>CheckpointManager: Success + CheckpointManager-->>CheckpointState: Return new manager + CheckpointState->>CheckpointState: Store new manager in map + CheckpointState-->>Backend: Return new manager + end + Backend->>CheckpointManager: create_checkpoint(description, ...) + CheckpointManager->>CheckpointManager: Read current messages + CheckpointManager->>Filesystem: Walk project directory + Filesystem-->>CheckpointManager: List of files + loop For each project file + CheckpointManager->>Filesystem: Read file content & metadata + Filesystem-->>CheckpointManager: File data + CheckpointManager->>CheckpointManager: Track file state (hash, modified) + end + CheckpointManager->>CheckpointStorage: save_checkpoint(checkpoint, snapshots, messages) + CheckpointStorage->>Filesystem: Write metadata.json, messages.jsonl (compressed) + loop For each modified file + CheckpointStorage->>Filesystem: Check if hash exists in content_pool + alt Hash exists + CheckpointStorage->>Filesystem: Skip writing content + else Hash does not exist + CheckpointStorage->>Filesystem: Write compressed file content to content_pool (by hash) + end + CheckpointStorage->>Filesystem: Write reference file (metadata + hash) to refs/ + end + CheckpointStorage->>Filesystem: Update timeline.json + Filesystem-->>CheckpointStorage: Success + CheckpointStorage-->>CheckpointManager: Return success/result + CheckpointManager-->>Backend: Return success/result + Backend-->>Frontend: Resolve Promise + Frontend->>Frontend: Call loadTimeline() to refresh UI + Frontend->>User: Display new checkpoint in timeline +``` + +This diagram illustrates the flow from the user clicking a button to the backend coordinating with the manager, which in turn uses the storage layer to read and write data to the filesystem, resulting in a new checkpoint entry and updated timeline on disk. + +### Restoring a Checkpoint Flow + +Restoring a checkpoint works in reverse. When the frontend calls `api.restoreCheckpoint(checkpointId, ...)`, the backend finds the `CheckpointManager` and calls `manager.restore_checkpoint(checkpointId)`. + +```rust +// src-tauri/src/checkpoint/manager.rs (Simplified) +// ... imports ... + +impl CheckpointManager { + // ... create_checkpoint() etc. ... + + /// Restore a checkpoint + pub async fn restore_checkpoint(&self, checkpoint_id: &str) -> Result { + // Load checkpoint data using the storage layer + let (checkpoint, file_snapshots, messages) = self.storage.load_checkpoint( + &self.project_id, + &self.session_id, + checkpoint_id, + )?; + + // Get list of all files currently in the project directory + let mut current_files = Vec::new(); + let _ = collect_all_project_files(&self.project_path, &self.project_path, &mut current_files); + + // Determine which files need to be deleted (exist now, but not in snapshot as non-deleted) + let mut checkpoint_files_set = std::collections::HashSet::new(); + for snapshot in &file_snapshots { + if !snapshot.is_deleted { + checkpoint_files_set.insert(snapshot.file_path.clone()); + } + } + + // Delete files not present (as non-deleted) in the checkpoint + for current_file in current_files { + if !checkpoint_files_set.contains(¤t_file) { + let full_path = self.project_path.join(¤t_file); + // ... attempt fs::remove_file(&full_path) ... + log::info!("Deleted file not in checkpoint: {:?}", current_file); + } + } + // ... attempt to remove empty directories ... + + + // Restore/overwrite files from snapshots + let mut files_processed = 0; + for snapshot in &file_snapshots { + // This helper handles creating parent dirs, writing content, setting permissions, or deleting + match self.restore_file_snapshot(snapshot).await { // Calls helper + Ok(_) => { /* ... */ }, + Err(e) => { /* ... collect warnings ... */ }, + } + files_processed += 1; + } + + // Update in-memory messages buffer + let mut current_messages = self.current_messages.write().await; + current_messages.clear(); + for line in messages.lines() { + current_messages.push(line.to_string()); + } + + // Update the current_checkpoint_id in the in-memory timeline + let mut timeline = self.timeline.write().await; + timeline.current_checkpoint_id = Some(checkpoint_id.to_string()); + + // Reset the file tracker state to match the restored checkpoint + let mut tracker = self.file_tracker.write().await; + tracker.tracked_files.clear(); // Clear old state + for snapshot in &file_snapshots { + if !snapshot.is_deleted { + tracker.tracked_files.insert( + snapshot.file_path.clone(), + FileState { + last_hash: snapshot.hash.clone(), + is_modified: false, // Assume clean state after restore + last_modified: Utc::now(), // Or snapshot timestamp if available? + exists: true, + } + ); + } + } + + + Ok(CheckpointResult { /* ... checkpoint, files_processed, warnings ... */ }) + } + + // Helper to restore a single file from its snapshot data + async fn restore_file_snapshot(&self, snapshot: &FileSnapshot) -> Result<()> { + let full_path = self.project_path.join(&snapshot.file_path); + + if snapshot.is_deleted { + // If snapshot indicates deleted, remove the file if it exists + if full_path.exists() { + fs::remove_file(&full_path).context("Failed to delete file")?; + } + } else { + // If snapshot exists, create parent directories and write content + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).context("Failed to create parent directories")?; + } + fs::write(&full_path, &snapshot.content).context("Failed to write file")?; + + // Restore permissions (Unix only) + #[cfg(unix)] + if let Some(mode) = snapshot.permissions { + use std::os::unix::fs::PermissionsExt; + let permissions = std::fs::Permissions::from_mode(mode); + fs::set_permissions(&full_path, permissions).context("Failed to set file permissions")?; + } + } + Ok(()) + } + + // ... other methods ... +} +``` + +The `restore_checkpoint` function reads the checkpoint data from disk using `CheckpointStorage::load_checkpoint`. It then gets a list of the *current* files in the project directory. By comparing the current files with the files present in the checkpoint snapshot, it identifies which files need to be deleted. It iterates through the snapshots, using `restore_file_snapshot` to either delete files or write their content back to the project directory, recreating parent directories and setting permissions as needed. Finally, it updates the in-memory message list and the current checkpoint pointer in the timeline manager. + +This process effectively reverts the project directory and the session's state to match the chosen checkpoint. + +### Forking + +Forking is implemented by first restoring the session to the chosen checkpoint and then immediately creating a *new* checkpoint from that restored state. The key is that the new checkpoint explicitly sets its `parent_checkpoint_id` to the checkpoint it forked *from*, causing the timeline to branch. + +### Automatic Checkpointing + +Automatic checkpointing is controlled by the `auto_checkpoint_enabled` flag and the `checkpoint_strategy` setting stored in the `SessionTimeline`. When a new message arrives in the session (handled by the streaming output processing, [Chapter 7]), the `CheckpointManager::should_auto_checkpoint` method is called. This checks the strategy. For example, if the strategy is `PerPrompt`, it checks if the message is a user prompt. If the strategy is `Smart`, it checks if the message indicates a potentially destructive tool use (like `write`, `edit`, `bash`). If `should_auto_checkpoint` returns `true`, the backend triggers the `create_checkpoint` flow described above. + +### Cleanup + +The `Cleanup` feature in the `CheckpointSettings.tsx` component calls a backend command that uses `CheckpointStorage::cleanup_old_checkpoints`. This function loads the timeline, sorts checkpoints chronologically, identifies checkpoints older than the `keep_count`, and removes their metadata and references from disk. Crucially, it then calls `CheckpointStorage::garbage_collect_content` to find any actual file content in the `content_pool` directory that is *no longer referenced by any remaining checkpoints* and deletes that orphaned content to free up disk space. + +## Conclusion + +In this chapter, we delved into **Checkpointing**, a powerful feature in `claudia` that provides version control for your Claude Code sessions. We learned that checkpoints save snapshots of both your session's message history and the state of your project files, organized into a visual timeline. + +We explored how you can use the UI to create manual checkpoints, restore to previous states, fork off new branches of work, view differences between checkpoints, and configure automatic checkpointing and cleanup settings. + +Under the hood, we saw how the backend uses a `CheckpointManager` per session, coordinates with `CheckpointStorage` for reading and writing to disk, tracks file changes using a `FileTracker`, and uses a content-addressable storage mechanism for file snapshots to save disk space. We walked through the steps involved in creating and restoring checkpoints, including managing file changes and updating the session state. + +Understanding checkpointing empowers you to use Claude Code for more complex and iterative tasks with confidence, knowing you can always revert to a previous state or explore different paths. + +In the next and final chapter, we will explore **MCP (Model Context Protocol)**, the standardized format Claude Code uses for exchanging information with tools and other components, which plays a role in enabling features like checkpointing and tool execution. + +[Next Chapter: MCP (Model Context Protocol)](10_mcp__model_context_protocol__.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/manager.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/mod.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/state.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/storage.rs), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CheckpointSettings.tsx), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/TimelineNavigator.tsx) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia_1.md b/Claudia-docs/V_1/claudia_1.md new file mode 100644 index 00000000..da2ff5ef --- /dev/null +++ b/Claudia-docs/V_1/claudia_1.md @@ -0,0 +1,1954 @@ + +# Chapter 1: Session/Project Management + +Welcome to the first chapter of the `claudia` tutorial! In this chapter, we'll explore how `claudia` helps you keep track of your work with Claude Code using its Session/Project Management feature. + +Imagine you're using Claude Code to help you build a new feature in a software project. You spend hours talking to Claude, asking it to write code, explain concepts, and debug issues. This interaction is a "session". Your feature development is happening within a specific folder on your computer – that's your "project". + +As you work on different projects, you'll have many sessions. How do you find that helpful conversation you had last week about a bug fix in your "website-redesign" project? How do you pick up where you left off? This is exactly what the Session/Project Management part of `claudia` solves! + +It's like having a digital filing cabinet for all your Claude Code conversations, organized by the project you were working on. + +## What are Projects and Sessions in `claudia`? + +At its core, Session/Project Management deals with two main ideas: + +1. **Projects:** A "Project" in `claudia` (and the underlying Claude Code CLI) corresponds to a directory on your computer where you were running Claude Code. When you start Claude Code in a folder, it recognizes that as a project. +2. **Sessions:** A "Session" is a single, continuous conversation you had with Claude Code within a specific project. Every time you run the `claude` command (or `claude --resume`, `claude --continue`), you're starting or continuing a session. + +The Claude Code CLI automatically saves your conversation history. `claudia` reads this saved history to show you what you've done. + +## Where is the Data Stored? + +The Claude Code CLI stores everything it needs inside a special directory in your home folder: `~/.claude`. + +Inside `~/.claude`, you'll find: + +* A `projects` directory: This is where information about your projects and their sessions is kept. +* Other files like `settings.json` or `CLAUDE.md` (we'll talk about settings and `CLAUDE.md` later). + +Each project you've worked on will have a subdirectory inside `~/.claude/projects`. The name of this subdirectory is a special encoded version of the project's file path. + +Inside a project's directory (`~/.claude/projects/your-project-id/`), you'll find files ending in `.jsonl`. Each `.jsonl` file is a single **session**. The name of the file (before `.jsonl`) is the unique ID for that session. These files contain a history of messages, commands, and tool outputs for that specific conversation. + +## How Does `claudia` Show You Your History? + +Let's look at the user interface of `claudia`. When you open it, you'll likely see a list of your recent projects. Clicking on a project takes you to a list of sessions within that project. You can then click on a session to view its history or resume it. + +Here's a simplified look at how the frontend components display this information: + +```typescript +// src/components/ProjectList.tsx - Simplified structure +import { Card, CardContent } from "@/components/ui/card"; +import type { Project } from "@/lib/api"; + +interface ProjectListProps { + projects: Project[]; // This array comes from the backend + onProjectClick: (project: Project) => void; +} + +export const ProjectList: React.FC = ({ projects, onProjectClick }) => { + return ( +
+ {/* Loop through the projects array */} + {projects.map((project) => ( + onProjectClick(project)}> + +
+

{project.path}

{/* Display the project path */} +

{project.sessions.length} sessions

{/* Show session count */} + {/* ... other project info like creation date */} +
+ {/* ... click handler */} +
+
+ ))} + {/* ... Pagination */} +
+ ); +}; +``` + +This component (`ProjectList.tsx`) takes a list of `Project` objects (fetched from the backend) and renders a card for each one, showing basic info like the project path and how many sessions it contains. When you click a card, it calls the `onProjectClick` function, typically navigating you to the sessions list for that project. + +Next, let's look at how the sessions for a selected project are displayed: + +```typescript +// src/components/SessionList.tsx - Simplified structure +import { Card, CardContent } from "@/components/ui/card"; +import type { Session } from "@/lib/api"; + +interface SessionListProps { + sessions: Session[]; // This array comes from the backend for the selected project + projectPath: string; + onSessionClick?: (session: Session) => void; + onBack: () => void; // Button to go back to project list +} + +export const SessionList: React.FC = ({ sessions, projectPath, onSessionClick, onBack }) => { + return ( +
+ {/* Back button */} +

{projectPath}

{/* Display the current project path */} +
+ {/* Loop through the sessions array */} + {sessions.map((session) => ( + onSessionClick?.(session)}> + +
+

Session ID: {session.id.slice(0, 8)}...

{/* Display truncated session ID */} + {/* Display the first message preview if available */} + {session.first_message &&

First msg: {session.first_message}

} + {/* ... other session info like timestamps */} +
+ {/* ... click handler */} +
+
+ ))} +
+ {/* ... Pagination */} +
+ ); +}; +``` + +The `SessionList.tsx` component receives the list of sessions for a *single* project (again, fetched from the backend). It shows you the project path you're currently viewing and lists each session, often including its ID, creation time, and a preview of the first message. Clicking a session calls `onSessionClick`, which will lead to the conversation view (`ClaudeCodeSession.tsx`). + +## How it Works: Under the Hood + +The frontend components we just saw need data to display. This data is provided by the backend code, which runs in Rust using the Tauri framework. The backend's job for Session/Project Management is to read the files in the `~/.claude` directory and structure that information for the frontend. + +Here's a simplified step-by-step of what happens when the frontend asks for the list of projects: + +1. The frontend calls a backend command, specifically `list_projects`. +2. The backend code starts by finding the `~/.claude` directory on your computer. +3. It then looks inside the `~/.claude/projects` directory. +4. For each directory found inside `projects`, it treats it as a potential project. +5. It reads the name of the project directory (which is an encoded path) and tries to find the actual project path by looking at the session files inside. +6. It also counts the number of `.jsonl` files (sessions) inside that project directory. +7. It gets the creation timestamp of the project directory. +8. It gathers this information (project ID, path, sessions list, creation time) into a `Project` struct. +9. It repeats this for all project directories. +10. Finally, it sends a list of these `Project` structs back to the frontend. + +Fetching sessions for a specific project follows a similar pattern: + +1. The frontend calls the `get_project_sessions` command, providing the `project_id`. +2. The backend finds the specific project directory inside `~/.claude/projects` using the provided `project_id`. +3. It looks inside that project directory for all `.jsonl` files. +4. For each `.jsonl` file (session), it extracts the session ID from the filename. +5. It gets the file's creation timestamp. +6. It reads the *first few lines* of the `.jsonl` file to find the first user message and its timestamp, for display as a preview in the UI. +7. It might also check for related files like todo data (`.json` files in `~/.claude/todos` linked by session ID). +8. It gathers this info into a `Session` struct. +9. It repeats this for all session files in the project directory. +10. Finally, it sends a list of `Session` structs back to the frontend. + +Here's a sequence diagram illustrating the `list_projects` flow: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Filesystem as ~/.claude + + User->>Frontend: Open Projects View + Frontend->>Backend: Call list_projects() + Backend->>Filesystem: Read ~/.claude/projects directory + Filesystem-->>Backend: List of project directories + Backend->>Filesystem: For each directory: Read contents (session files) + Filesystem-->>Backend: List of session files + Backend->>Backend: Process directories and files (create Project structs) + Backend-->>Frontend: Return List + Frontend->>User: Display Project List +``` + +And the `get_project_sessions` flow: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Filesystem as ~/.claude + + User->>Frontend: Click on a Project + Frontend->>Backend: Call get_project_sessions(projectId) + Backend->>Filesystem: Read ~/.claude/projects/projectId/ directory + Filesystem-->>Backend: List of session files (.jsonl) + Backend->>Filesystem: For each session file: Read first lines, read metadata + Filesystem-->>Backend: First message, timestamp, creation time, etc. + Backend->>Backend: Process session files (create Session structs) + Backend-->>Frontend: Return List + Frontend->>User: Display Session List for Project +``` + +## Diving into the Code + +Let's look at some specific parts of the Rust code in `src-tauri/src/commands/claude.rs` that handle this logic. + +First, the data structures that represent a project and a session: + +```rust +// src-tauri/src/commands/claude.rs +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub id: String, // The encoded directory name + pub path: String, // The decoded or detected real path + pub sessions: Vec, // List of session file names (IDs) + pub created_at: u64, // Timestamp +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + pub id: String, // The session file name (UUID) + pub project_id: String, // Link back to the project + pub project_path: String, // The project's real path + pub todo_data: Option, // Optional associated data + pub created_at: u64, // Timestamp + pub first_message: Option, // Preview of the first user message + pub message_timestamp: Option, // Timestamp of the first message +} +// ... rest of the file +``` + +These `struct` definitions tell us what information the backend collects and sends to the frontend for projects and sessions. Notice the `Serialize` and `Deserialize` derives; this is what allows Tauri to easily pass these structures between the Rust backend and the JavaScript/TypeScript frontend. + +Here's the function that finds the base `~/.claude` directory: + +```rust +// src-tauri/src/commands/claude.rs +fn get_claude_dir() -> Result { + dirs::home_dir() // Find the user's home directory + .context("Could not find home directory")? // Handle potential error + .join(".claude") // Append the .claude directory name + .canonicalize() // Resolve symbolic links, etc. + .context("Could not find ~/.claude directory") // Handle potential error +} +// ... rest of the file +``` + +This simple function is crucial as all project and session data is located relative to `~/.claude`. + +Now, a look at the `list_projects` function. We'll skip some error handling and logging for brevity here: + +```rust +// src-tauri/src/commands/claude.rs +#[tauri::command] +pub async fn list_projects() -> Result, String> { + let claude_dir = get_claude_dir().map_err(|e| e.to_string())?; + let projects_dir = claude_dir.join("projects"); // Path to ~/.claude/projects + + if !projects_dir.exists() { + return Ok(Vec::new()); // Return empty list if directory doesn't exist + } + + let mut projects = Vec::new(); + + // Iterate over entries inside ~/.claude/projects + let entries = fs::read_dir(&projects_dir).map_err(|e| format!("..."))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("..."))?; + let path = entry.path(); + + if path.is_dir() { // Only process directories + let dir_name = path.file_name().and_then(|n| n.to_str()).ok_or_else(|| "...").unwrap(); + + // Get creation/modification timestamp + let metadata = fs::metadata(&path).map_err(|e| format!("..."))?; + let created_at = metadata.created().or_else(|_| metadata.modified()).unwrap_or(SystemTime::UNIX_EPOCH).duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); + + // Determine the actual project path (explained next) + let project_path = match get_project_path_from_sessions(&path) { + Ok(p) => p, + Err(_) => decode_project_path(dir_name) // Fallback if session files don't exist + }; + + // Find all session files (.jsonl) in this project directory + let mut sessions = Vec::new(); + if let Ok(session_entries) = fs::read_dir(&path) { + for session_entry in session_entries.flatten() { + let session_path = session_entry.path(); + if session_path.is_file() && session_path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + if let Some(session_id) = session_path.file_stem().and_then(|s| s.to_str()) { + sessions.push(session_id.to_string()); // Store session ID (filename) + } + } + } + } + + // Add the project to the list + projects.push(Project { + id: dir_name.to_string(), + path: project_path, + sessions, + created_at, + }); + } + } + + projects.sort_by(|a, b| b.created_at.cmp(&a.created_at)); // Sort newest first + Ok(projects) +} +// ... rest of the file +``` + +This code reads the `projects` directory, identifies subdirectories as projects, and collects basic information for each. A key part is determining the *actual* project path, as the directory name is an encoded version of the path where Claude Code was run. The `get_project_path_from_sessions` function handles this: + +```rust +// src-tauri/src/commands/claude.rs +fn get_project_path_from_sessions(project_dir: &PathBuf) -> Result { + // Try to read any JSONL file in the directory + let entries = fs::read_dir(project_dir) + .map_err(|e| format!("..."))?; + + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + // Read the first line of the JSONL file + if let Ok(file) = fs::File::open(&path) { + let reader = BufReader::new(file); + if let Some(Ok(first_line)) = reader.lines().next() { + // Parse the JSON and extract "cwd" (current working directory) + if let Ok(json) = serde_json::from_str::(&first_line) { + if let Some(cwd) = json.get("cwd").and_then(|v| v.as_str()) { + return Ok(cwd.to_string()); // Found the project path! + } + } + } + } + } + } + } + + Err("Could not determine project path from session files".to_string()) // Failed to find it +} +// ... rest of the file +``` + +This function is smarter than just decoding the directory name. It opens the first session file it finds within a project directory, reads the very first line (which usually contains metadata including the `cwd` - current working directory - where Claude Code was launched), and uses that `cwd` as the definitive project path. This is more reliable than trying to decode the directory name. + +Finally, let's look at `get_project_sessions`: + +```rust +// src-tauri/src/commands/claude.rs +#[tauri::command] +pub async fn get_project_sessions(project_id: String) -> Result, String> { + let claude_dir = get_claude_dir().map_err(|e| e.to_string())?; + let project_dir = claude_dir.join("projects").join(&project_id); // Path to specific project dir + let todos_dir = claude_dir.join("todos"); // Path to todo data + + if !project_dir.exists() { + return Err(format!("Project directory not found: {}", project_id)); + } + + // Determine the actual project path + let project_path = match get_project_path_from_sessions(&project_dir) { + Ok(p) => p, + Err(_) => decode_project_path(&project_id) // Fallback + }; + + let mut sessions = Vec::new(); + + // Read all files in the project directory + let entries = fs::read_dir(&project_dir).map_err(|e| format!("..."))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("..."))?; + let path = entry.path(); + + // Process only .jsonl files + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + if let Some(session_id) = path.file_stem().and_then(|s| s.to_str()) { // Get filename as session ID + // Get file creation timestamp + let metadata = fs::metadata(&path).map_err(|e| format!("..."))?; + let created_at = metadata.created().or_else(|_| metadata.modified()).unwrap_or(SystemTime::UNIX_EPOCH).duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); + + // Extract first user message for preview (explained next) + let (first_message, message_timestamp) = extract_first_user_message(&path); + + // Check for associated todo data file + let todo_path = todos_dir.join(format!("{}.json", session_id)); + let todo_data = if todo_path.exists() { + // ... read and parse todo.json ... + None // Simplified: just show if it exists, not the data + } else { + None + }; + + // Add the session to the list + sessions.push(Session { + id: session_id.to_string(), + project_id: project_id.clone(), + project_path: project_path.clone(), + todo_data, + created_at, + first_message, + message_timestamp, + }); + } + } + } + + sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); // Sort newest first + Ok(sessions) +} +// ... rest of the file +``` + +This function is similar to `list_projects` but focuses on one project directory. It iterates through the files, identifies the `.jsonl` session files, extracts metadata like ID and timestamp, and importantly, calls `extract_first_user_message` to get a quick preview of the conversation's start for the UI. + +The `extract_first_user_message` function reads the session's `.jsonl` file line by line, parses each line as JSON, and looks for the first entry that represents a message from the "user" role, making sure to skip certain types of messages (like the initial system caveat or command outputs) to find the actual user prompt. + +## Putting it Together + +So, the Session/Project Management feature in `claudia` works by: + +1. Reading the file structure created by the Claude Code CLI in `~/.claude`. +2. Identifying directories in `~/.claude/projects` as projects and `.jsonl` files within them as sessions. +3. Extracting key metadata (IDs, paths, timestamps, first message previews). +4. Providing this structured data to the frontend UI via Tauri commands (`list_projects`, `get_project_sessions`). +5. Allowing the frontend (`ProjectList.tsx`, `SessionList.tsx`) to display this information in an organized, browsable way. +6. Enabling the user to select a session, triggering navigation to the main session view (`ClaudeCodeSession.tsx`) where they can see the full history (loaded using `load_session_history`) and potentially resume the conversation. + +This abstraction provides the essential foundation for interacting with your past Claude Code work, allowing you to manage your conversation history effectively. + +## Conclusion + +In this chapter, we learned how `claudia` discovers, lists, and displays your Claude Code projects and sessions by reading files from the `~/.claude` directory. We saw how the frontend components like `ProjectList` and `SessionList` use data provided by backend commands like `list_projects` and `get_project_sessions` to build the navigation interface. We also briefly touched upon how session data (`.jsonl` files) is parsed to show previews. + +Understanding how `claudia` manages sessions and projects is the first step in seeing how it builds a rich user interface on top of the command-line tool. In the next chapter, we'll dive into the concept of [Agents](02_agents_.md), which are central to how Claude Code and `claudia` understand the context of your work. + +[Next Chapter: Agents](02_agents_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/claude.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ProjectList.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/SessionList.tsx) +# Chapter 2: Agents + +Welcome back to the `claudia` tutorial! In [Chapter 1: Session/Project Management](01_session_project_management_.md), we learned how `claudia` helps you keep track of your conversations with Claude Code by organizing them into projects and sessions stored in the `~/.claude` directory. + +Now that you know how to find your past work, let's talk about the next key concept in `claudia`: **Agents**. + +## What is an Agent? + +Imagine you use Claude Code for different kinds of tasks. Sometimes you need it to act as a strict code reviewer, sometimes as a creative brainstorming partner, and other times as a focused debugger. Each task might require Claude to have a different "personality" or set of instructions. + +Instead of typing out the same long system prompt (the initial instructions you give to Claude) every time, `claudia` lets you save these configurations as **Agents**. + +Think of an Agent as a pre-packaged, specialized assistant you create within `claudia`. Each Agent is designed for a specific purpose, with its own instructions and capabilities already defined. + +**In simpler terms:** + +* An Agent is like a saved profile for how you want Claude Code to behave. +* You give it a name (like "Bug Hunter" or "Documentation Writer"). +* You give it an icon to easily spot it. +* You give it a "System Prompt" - this is the set of rules or instructions that tell Claude how to act for this specific Agent. For example, a "Bug Hunter" agent might have a system prompt like, "You are an expert Python debugger. Analyze the provided code snippets for potential bugs, common errors, and suggest fixes." +* You can set what permissions it has (like if it's allowed to read or write files). +* You choose which Claude model it should use (like Sonnet or Opus). + +Once an Agent is created, you can select it, give it a specific task (like "debug the function in `main.py`"), choose a project directory, and hit "Execute". `claudia` then runs the Claude Code CLI using *that Agent's* configuration. + +This is much more efficient than manually setting options every time you use Claude Code for a particular job! + +## Key Parts of an Agent + +Let's break down the core components that make up an Agent in `claudia`. You'll configure these when you create or edit an Agent: + +| Part | Description | Why it's important | +| :-------------- | :-------------------------------------------------------------------------- | :-------------------------------------------------- | +| **Name** | A human-readable label (e.g., "Code Reviewer", "Creative Writer"). | Helps you identify the Agent. | +| **Icon** | A visual symbol (e.g., 🤖, ✨, 🛠️). | Makes it easy to find the right Agent at a glance. | +| **System Prompt** | The core instructions given to Claude at the start of the conversation. | Defines the Agent's role, personality, and rules. | +| **Model** | Which Claude model (e.g., Sonnet, Opus) the Agent should use. | Affects performance, capabilities, and cost. | +| **Permissions** | Controls what the Agent is allowed to do (file read/write, network). | **Crucial for security** when running code or tools. | +| **Default Task**| Optional pre-filled text for the task input field when running the Agent. | Saves time for common tasks with this Agent. | + +## Creating and Managing Agents + +`claudia` provides a friendly user interface for managing your Agents. You'll typically find this in the main menu under something like "CC Agents". + +### The Agents List + +When you go to the Agents section, you'll see a list (or grid) of all the Agents you've created. + +You can see their name, icon, and options to: + +* **Execute:** Run the Agent with a new task. +* **Edit:** Change the Agent's configuration. +* **Delete:** Remove the Agent. +* **Create:** Add a brand new Agent. + +Let's look at a simplified frontend component (`CCAgents.tsx`) that displays this list: + +```typescript +// src/components/CCAgents.tsx (Simplified) +// ... imports ... +export const CCAgents: React.FC = ({ onBack, className }) => { + const [agents, setAgents] = useState([]); + // ... state for loading, view mode, etc. ... + + useEffect(() => { + // Fetch agents from the backend when the component loads + const loadAgents = async () => { + try { + const agentsList = await api.listAgents(); // Call backend API + setAgents(agentsList); + } catch (err) { + console.error("Failed to load agents:", err); + } + }; + loadAgents(); + }, []); + + // ... handleDeleteAgent, handleEditAgent, handleExecuteAgent functions ... + // ... state for pagination ... + + return ( + // ... layout code ... + {/* Agents Grid */} +
+ {/* Loop through the fetched agents */} + {agents.map((agent) => ( + + +
{/* Render agent icon */}
+

{agent.name}

+ {/* ... other agent info ... */} +
+ + {/* Buttons to Execute, Edit, Delete */} + + + + +
+ ))} +
+ // ... pagination and other UI elements ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This simplified code shows how the `CCAgents` component fetches a list of `Agent` objects from the backend using `api.listAgents()` and then displays them in cards, providing buttons for common actions. + +### Creating or Editing an Agent + +Clicking "Create" or "Edit" takes you to a different view (`CreateAgent.tsx`). Here, you'll find a form where you can fill in the details of the Agent: name, choose an icon, write the system prompt, select the model, set permissions, and add an optional default task. + +A snippet from the `CreateAgent.tsx` component: + +```typescript +// src/components/CreateAgent.tsx (Simplified) +// ... imports ... +export const CreateAgent: React.FC = ({ + agent, // If provided, we are editing + onBack, + onAgentCreated, + className, +}) => { + const [name, setName] = useState(agent?.name || ""); + const [systemPrompt, setSystemPrompt] = useState(agent?.system_prompt || ""); + // ... state for icon, model, permissions, etc. ... + + const isEditMode = !!agent; + + const handleSave = async () => { + // ... validation ... + try { + // ... set saving state ... + if (isEditMode && agent.id) { + // Call backend API to update agent + await api.updateAgent(agent.id, name, /* ... other fields ... */ systemPrompt, /* ... */); + } else { + // Call backend API to create new agent + await api.createAgent(name, /* ... other fields ... */ systemPrompt, /* ... */); + } + onAgentCreated(); // Notify parent component + } catch (err) { + console.error("Failed to save agent:", err); + // ... show error ... + } finally { + // ... unset saving state ... + } + }; + + // ... handleBack function with confirmation ... + + return ( + // ... layout code ... +
+ {/* Header with Back and Save buttons */} +
+ +

{isEditMode ? "Edit CC Agent" : "Create CC Agent"}

+ +
+ + {/* Form fields */} +
+ {/* Name Input */} +
+ + setName(e.target.value)} /> +
+ + {/* Icon Picker */} + {/* ... component for selecting icon ... */} + + {/* Model Selection */} + {/* ... buttons/radios for model selection ... */} + + {/* Default Task Input */} + {/* ... input for default task ... */} + + {/* Sandbox Settings (Separate Component) */} + + + {/* System Prompt Editor */} +
+ + {/* ... MDEditor component for system prompt ... */} +
+
+
+ // ... Toast Notification ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This component manages the state for the agent's properties and calls either `api.createAgent` or `api.updateAgent` from the backend API layer when the "Save" button is clicked. + +Notice the inclusion of `AgentSandboxSettings`. This is a smaller component (`AgentSandboxSettings.tsx`) specifically for managing the permission toggles: + +```typescript +// src/components/AgentSandboxSettings.tsx (Simplified) +// ... imports ... +export const AgentSandboxSettings: React.FC = ({ + agent, // Receives the current agent state + onUpdate, // Callback to notify parent of changes + className +}) => { + // ... handleToggle function ... + + return ( + + {/* ... Header with Shield icon ... */} +
+ {/* Master sandbox toggle */} +
+ + handleToggle('sandbox_enabled', checked)} // Update parent state + /> +
+ + {/* Permission toggles - conditional render */} + {agent.sandbox_enabled && ( +
+ {/* File Read Toggle */} +
+ + handleToggle('enable_file_read', checked)} // Update parent state + /> +
+ {/* File Write Toggle */} +
+ + handleToggle('enable_file_write', checked)} // Update parent state + /> +
+ {/* Network Toggle */} +
+ + handleToggle('enable_network', checked)} // Update parent state + /> +
+
+ )} + {/* ... Warning when sandbox disabled ... */} +
+
+ ); +}; +``` + +This component simply displays the current sandbox settings for the agent and provides switches to toggle them. When a switch is toggled, it calls the `onUpdate` prop to inform the parent (`CreateAgent`) component, which manages the overall agent state. + +## Executing an Agent + +Once you have agents created, the main purpose is to *run* them. Selecting an agent from the list and clicking "Execute" (or the Play button) takes you to the Agent Execution view (`AgentExecution.tsx`). + +Here's where you: + +1. Select a **Project Path**: This is the directory where the agent will run and where it can potentially read/write files (subject to its permissions). This ties back to the projects we discussed in [Chapter 1: Session/Project Management](01_session_project_management_.md). +2. Enter the **Task**: This is the specific request you have for the agent *for this particular run*. +3. (Optional) Override the **Model**: Choose a different model (Sonnet/Opus) just for this run if needed. +4. Click **Execute**. + +The `AgentExecution.tsx` component handles this: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +export const AgentExecution: React.FC = ({ + agent, // The agent being executed + onBack, + className, +}) => { + const [projectPath, setProjectPath] = useState(""); + const [task, setTask] = useState(""); + const [model, setModel] = useState(agent.model || "sonnet"); // Default to agent's model + const [isRunning, setIsRunning] = useState(false); + const [messages, setMessages] = useState([]); // Output messages + // ... state for stats, errors, etc. ... + + // ... useEffect for listeners and timers ... + + const handleSelectPath = async () => { + // Use Tauri dialog to select a directory + const selected = await open({ directory: true, multiple: false }); + if (selected) { + setProjectPath(selected as string); + } + }; + + const handleExecute = async () => { + if (!projectPath || !task.trim()) return; // Basic validation + + try { + setIsRunning(true); + setMessages([]); // Clear previous output + // ... reset stats, setup listeners ... + + // Call backend API to execute the agent + await api.executeAgent(agent.id!, projectPath, task, model); + + } catch (err) { + console.error("Failed to execute agent:", err); + // ... show error, update state ... + } + }; + + // ... handleStop, handleBackWithConfirmation functions ... + + return ( + // ... layout code ... +
+ {/* Header with Back button and Agent Name */} +
+ +

{agent.name}

+ {/* ... Running status indicator ... */} +
+ + {/* Configuration Section */} +
+ {/* ... Error display ... */} + {/* Project Path Input with Select Button */} +
+ + setProjectPath(e.target.value)} disabled={isRunning} /> + +
+ {/* Model Selection Buttons */} + {/* ... buttons for Sonnet/Opus selection ... */} + {/* Task Input with Execute/Stop Button */} +
+ + setTask(e.target.value)} disabled={isRunning} /> + +
+
+ + {/* Output Display Section */} +
+ {/* Messages are displayed here, streaming as they arrive */} + {/* ... Rendering messages using StreamMessage component ... */} +
+ + {/* Floating Execution Control Bar */} + {/* ... Component showing elapsed time, tokens, etc. ... */} +
+ // ... Fullscreen Modal ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This component uses the `api.executeAgent` Tauri command to start the agent's run. It also sets up event listeners (`agent-output`, `agent-error`, `agent-complete`) to receive data and status updates from the backend *while* the agent is running. This streaming output is then displayed to the user, which we'll cover in more detail in [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md). + +## How it Works: Under the Hood + +Let's peek behind the curtain to understand how `claudia` handles Agents in the backend (Rust code). + +### Agent Storage + +Unlike projects and sessions which are managed by the Claude Code CLI itself in the filesystem (`~/.claude`), `claudia` stores its Agent definitions in a local SQLite database file, typically located within `claudia`'s application data directory (e.g., `~/.config/claudia/agents.db` on Linux, or similar paths on macOS/Windows). + +The `Agent` struct in the Rust backend corresponds to the data stored for each agent: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Agent { + pub id: Option, // Database ID + pub name: String, + pub icon: String, + pub system_prompt: String, + pub default_task: Option, + pub model: String, // e.g., "sonnet", "opus" + // Permissions managed directly on the agent struct + pub sandbox_enabled: bool, + pub enable_file_read: bool, + pub enable_file_write: bool, + pub enable_network: bool, + pub created_at: String, + pub updated_at: String, +} +// ... rest of the file +``` + +The database initialization (`init_database` function) creates the `agents` table to store this information. Backend functions like `list_agents`, `create_agent`, `update_agent`, and `delete_agent` interact with this SQLite database to perform the requested actions. They simply execute standard SQL commands (SELECT, INSERT, UPDATE, DELETE) to manage the `Agent` records. + +Here's a tiny snippet showing a database interaction (listing agents): + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[tauri::command] +pub async fn list_agents(db: State<'_, AgentDb>) -> Result, String> { + let conn = db.0.lock().map_err(|e| e.to_string())?; // Get database connection + + let mut stmt = conn + .prepare("SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents ORDER BY created_at DESC") + .map_err(|e| e.to_string())?; // Prepare SQL query + + let agents = stmt + .query_map([], |row| { // Map database rows to Agent structs + Ok(Agent { + id: Some(row.get(0)?), + name: row.get(1)?, + // ... map other fields ... + system_prompt: row.get(3)?, + model: row.get::<_, String>(5).unwrap_or_else(|_| "sonnet".to_string()), + sandbox_enabled: row.get::<_, bool>(6).unwrap_or(true), + enable_file_read: row.get::<_, bool>(7).unwrap_or(true), + enable_file_write: row.get::<_, bool>(8).unwrap_or(true), + enable_network: row.get::<_, bool>(9).unwrap_or(false), + created_at: row.get(10)?, + updated_at: row.get(11)?, + }) + }) + .map_err(|e| e.to_string())? + .collect::, _>>() + .map_err(|e| e.to_string())?; + + Ok(agents) // Return the list of Agent structs +} +``` + +This snippet shows how `list_agents` connects to the database, prepares a simple `SELECT` statement, and then uses `query_map` to convert each row returned by the database into an `Agent` struct, which is then sent back to the frontend. + +### Agent Execution Flow + +When you click "Execute" for an Agent: + +1. The frontend (`AgentExecution.tsx`) calls the backend command `execute_agent` ([Chapter 4: Tauri Commands](04_tauri_commands_.md)), passing the agent's ID, the selected project path, and the entered task. +2. The backend receives the call and retrieves the full details of the selected Agent from the database. +3. It creates a record in the `agent_runs` database table. This table keeps track of each individual execution run of an agent, including which agent was run, the task given, the project path, and its current status (pending, running, completed, failed, cancelled). This links back to the run history shown in the `CCAgents.tsx` component and managed by the `AgentRun` struct: + ```rust + // src-tauri/src/commands/agents.rs (Simplified) + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct AgentRun { + pub id: Option, // Database ID for this run + pub agent_id: i64, // Foreign key linking to the Agent + pub agent_name: String, // Stored for convenience + pub agent_icon: String, // Stored for convenience + pub task: String, // The task given for this run + pub model: String, // The model used for this run + pub project_path: String, // The directory where it was executed + pub session_id: String, // The UUID from the Claude Code CLI session + pub status: String, // 'pending', 'running', 'completed', 'failed', 'cancelled' + pub pid: Option, // Process ID if running + pub process_started_at: Option, + pub created_at: String, + pub completed_at: Option, + } + ``` + When the run starts, the status is set to 'running', and the Process ID (PID) is recorded. +4. Based on the Agent's configured permissions (`enable_file_read`, `enable_file_write`, `enable_network`), the backend constructs a sandbox profile. This process involves defining rules that the operating system will enforce to limit what the `claude` process can access or do. This is a core part of the [Sandboxing](06_sandboxing_.md) concept. +5. The backend prepares the command to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). It includes arguments like: + * `-p "the task"` + * `--system-prompt "the agent's system prompt"` + * `--model "the selected model"` + * `--output-format stream-json` (to get structured output) + * `--dangerously-skip-permissions` (since `claudia` manages permissions via the sandbox, it tells `claude` not to ask the user). + * The command is also set to run in the specified project directory. +6. The backend then *spawns* the `claude` process within the sandbox environment. +7. As the `claude` process runs, its standard output (stdout) and standard error (stderr) streams are captured by the backend ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)). +8. The backend processes this output. For JSONL output from Claude Code, it extracts information like message content and session IDs. +9. It emits events back to the frontend (`agent-output`, `agent-error`) using the Tauri event system. +10. The frontend (`AgentExecution.tsx`) listens for these events and updates the displayed messages in real-time. +11. The backend also detects when the `claude` process finishes (either successfully, with an error, or if killed). +12. When the process finishes, the backend updates the `agent_runs` record in the database, setting the status to 'completed', 'failed', or 'cancelled' and recording the completion timestamp. + +Here's a simplified sequence diagram for Agent execution: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI (AgentExecution.tsx) + participant Backend as Tauri Commands (agents.rs) + participant Database as agents.db + participant Sandbox + participant ClaudeCLI as claude binary + + User->>Frontend: Clicks "Execute Agent" + Frontend->>Backend: Call execute_agent(agentId, path, task, model) + Backend->>Database: Read Agent config by ID + Database-->>Backend: Return Agent config + Backend->>Database: Create AgentRun record (status=pending/running) + Database-->>Backend: Return runId + Backend->>Sandbox: Prepare environment based on Agent permissions + Sandbox-->>Backend: Prepared environment/command + Backend->>ClaudeCLI: Spawn process (with task, prompt, model, in sandbox, in project path) + ClaudeCLI-->>Backend: Stream stdout/stderr (JSONL) + Backend->>Frontend: Emit "agent-output" events (parsed messages) + Frontend->>User: Display messages in UI + ClaudeCLI-->>Backend: Process finishes + Backend->>Database: Update AgentRun record (status=completed/failed/cancelled) + Database-->>Backend: Confirmation + Backend->>Frontend: Emit "agent-complete" event + Frontend->>User: Update UI (execution finished) +``` + +This diagram illustrates how the frontend initiates the run, the backend fetches the agent's configuration, prepares the environment (including sandbox rules), launches the `claude` process, captures its output, and updates the UI and database based on the process's progress and completion. + +## Conclusion + +In this chapter, we introduced the concept of **Agents** in `claudia`. We learned that Agents are customizable configurations for the Claude Code CLI, allowing you to define specific roles, instructions (System Prompt), models, and crucially, permissions for different types of tasks. + +We saw how the `claudia` UI allows you to easily create, edit, list, and execute these Agents, and how the backend stores Agent definitions in a local database. We also got a high-level view of the execution process, understanding that `claudia` launches the `claude` binary with the Agent's settings and captures its output. A key part of this is the preparation of a secure execution environment based on the Agent's defined permissions, which introduces the idea of sandboxing. + +Understanding Agents is fundamental, as they are the primary way you'll interact with Claude Code through `claudia` for structured tasks. In the next chapter, we'll zoom out and look at how the different visual parts of the `claudia` application you've seen connect together – diving into [Frontend UI Components](03_frontend_ui_components_.md). + +[Next Chapter: Frontend UI Components](03_frontend_ui_components_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[3]] +``` +(https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentSandboxSettings.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CCAgents.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CreateAgent.tsx) + +# Chapter 3: Frontend UI Components + +Welcome back to the `claudia` tutorial! In [Chapter 1: Session/Project Management](01_session_project_management_.md), we explored how `claudia` keeps track of your conversations. In [Chapter 2: Agents](02_agents_.md), we learned about creating and managing specialized configurations for Claude Code tasks. + +Now, let's shift our focus to what you actually *see* and *interact with* when you use `claudia`: its graphical interface. This interface is built using **Frontend UI Components**. + +## What are Frontend UI Components? + +Imagine building something complex, like a house. You don't start by crafting every tiny screw and nail from raw metal. Instead, you use pre-made bricks, windows, doors, and roof tiles. These are like reusable building blocks. + +Frontend UI Components in `claudia` are exactly like these building blocks, but for the visual parts of the application. They are self-contained pieces of the user interface, like: + +* A **Button** you click. +* A **Card** that displays information (like a project or an agent). +* An **Input** field where you type text. +* A **List** that shows multiple items. +* A **Dialog** box that pops up. + +`claudia` uses a popular web development framework called **React** to build these components. They are written using **TypeScript** (which adds type safety) and styled using **Tailwind CSS** (a way to add styles quickly using special class names). + +The key idea is reusability. Instead of designing a button from scratch every time it's needed, you create a `Button` component once and use it everywhere. This makes the UI consistent and development faster. + +## Building Views by Combining Components + +Just like you combine bricks and windows to build a wall, `claudia` combines different UI components to create full views (pages) of the application. + +For example, the list of projects you saw in Chapter 1 is a view. This view isn't one giant piece of code; it's made by combining: + +* `Button` components (like the "Back to Home" button). +* `Card` components, each displaying information about a single project. +* A `ProjectList` component which *contains* all the individual project `Card`s and handles looping through the list of projects. +* Layout components (like `div`s with Tailwind classes) to arrange everything. + +Let's look at a simplified structure of the `App.tsx` file, which acts like the main blueprint for `claudia`'s views. It decides *which* major component (view) to show based on the current state (`view` variable): + +```typescript +// src/App.tsx (Simplified) +import { useState } from "react"; +import { Button } from "@/components/ui/button"; // Import a UI component +import { Card } from "@/components/ui/card"; // Import another UI component +import { ProjectList } from "@/components/ProjectList"; // Import a view component +import { CCAgents } from "@/components/CCAgents"; // Import another view component +// ... other imports ... + +type View = "welcome" | "projects" | "agents" | "settings" | "claude-code-session"; + +function App() { + const [view, setView] = useState("welcome"); // State variable to control current view + // ... other state variables ... + + const renderContent = () => { + switch (view) { + case "welcome": + // Show the welcome view, using Card and Button components + return ( +
{/* Layout */} + setView("agents")}> {/* Uses Card */} +
+ {/* Icon component */} +

CC Agents

+
+
+ setView("projects")}> {/* Uses Card */} +
+ {/* Icon component */} +

CC Projects

+
+
+
+ ); + + case "agents": + // Show the Agents view, which is handled by the CCAgents component + return setView("welcome")} />; // Uses CCAgents component + + case "projects": + // Show the Projects/Sessions view + return ( +
{/* Layout */} + {/* Uses Button */} + {/* ... displays either ProjectList or SessionList based on selectedProject state ... */} +
+ ); + + // ... other cases for settings, session view, etc. ... + + default: + return null; + } + }; + + return ( +
+ {/* Topbar component */} + {/* Main content area */} +
+ {renderContent()} {/* Renders the selected view */} +
+ {/* ... other global components like dialogs ... */} +
+ ); +} + +export default App; +``` + +As you can see, `App.tsx` doesn't contain the detailed code for *every* button or card. Instead, it imports and uses components like `Button`, `Card`, `CCAgents`, and `ProjectList`. The `renderContent` function simply decides which larger component to display based on the `view` state. + +## How Components Work Together + +Components communicate with each other primarily through **props** (short for properties) and **callbacks** (functions passed as props). + +* **Props:** Data is passed *down* from parent components to child components using props. For example, the `App` component might pass the list of `projects` to the `ProjectList` component. The `ProjectList` component then passes individual `project` objects down to the `Card` components it renders. +* **Callbacks:** When something happens inside a child component (like a button click), it needs to tell its parent. It does this by calling a function that was passed down as a prop (a callback). For example, when a `Card` in the `ProjectList` is clicked, it calls the `onProjectClick` function that was given to it by `ProjectList`. `ProjectList` received this function from `App`. + +Let's revisit the `ProjectList` component from Chapter 1: + +```typescript +// src/components/ProjectList.tsx (Simplified) +// ... imports ... +import { Card, CardContent } from "@/components/ui/card"; // Uses Card component +import type { Project } from "@/lib/api"; + +interface ProjectListProps { + projects: Project[]; // Prop: receives array of project data + onProjectClick: (project: Project) => void; // Prop: receives a function (callback) +} + +export const ProjectList: React.FC = ({ projects, onProjectClick }) => { + return ( +
+ {/* Loops through the projects array received via props */} + {projects.map((project) => ( + // Renders a Card component for each project + onProjectClick(project)}> {/* Calls the onProjectClick callback when clicked */} + {/* Uses CardContent sub-component */} +
+

{project.path}

{/* Displays data received from the project prop */} +

{project.sessions.length} sessions

{/* Displays data from the project prop */} +
+
+
+ ))} +
+ ); +}; +``` + +This component clearly shows: +1. It receives data (`projects` array) and a function (`onProjectClick`) as props. +2. It loops through the `projects` array. +3. For each item, it renders a `Card` component (another UI component). +4. It passes data (`project.path`, `project.sessions.length`) into the `CardContent` to be displayed. +5. It attaches an `onClick` handler to the `Card` that calls the `onProjectClick` callback function, passing the relevant `project` data back up to the parent component (`App` in this case). + +Similarly, the `CCAgents` component from Chapter 2 receives data and callbacks: + +```typescript +// src/components/CCAgents.tsx (Simplified) +// ... imports ... +import { Card, CardContent, CardFooter } from "@/components/ui/card"; // Uses Card components +import { Button } from "@/components/ui/button"; // Uses Button component +// ... types and state ... + +export const CCAgents: React.FC = ({ onBack, className }) => { + // ... state for agents data ... + + // ... useEffect to load agents (calls backend, covered in Chapter 2) ... + + // Callback functions for actions + const handleExecuteAgent = (agent: Agent) => { + // ... navigate to execution view ... + }; + const handleEditAgent = (agent: Agent) => { + // ... navigate to edit view ... + }; + const handleDeleteAgent = (agentId: number) => { + // ... call backend API to delete ... + }; + + return ( +
+ {/* ... Back button using Button component calling onBack prop ... */} + + {/* Agents Grid */} +
+ {/* Loop through agents state */} + {agents.map((agent) => ( + {/* Uses Card */} + {/* Uses CardContent */} + {/* ... display agent icon, name (data from agent state) ... */} + + {/* Uses CardFooter */} + {/* Buttons using Button component, calling local callbacks */} + + + + + + ))} +
+ {/* ... pagination ... */} +
+ ); +}; +``` + +This component shows how UI components (`Card`, `Button`) are used within a larger view component (`CCAgents`). `CCAgents` manages its own state (the list of `agents`) and defines callback functions (`handleExecuteAgent`, `handleEditAgent`, `handleDeleteAgent`) which are triggered by user interaction with the child `Button` components. It also receives an `onBack` prop from its parent (`App`) to navigate back. + +## Common UI Components in `claudia` + +`claudia` uses a set of pre-built, simple UI components provided by a library often referred to as "shadcn/ui" (though integrated directly into the project). You saw some examples in the code: + +* **`Button`**: Used for clickable actions (`components/ui/button.tsx`). +* **`Card`**: Used to group related information with a border and shadow (`components/ui/card.tsx`). It often has `CardHeader`, `CardContent`, and `CardFooter` sub-components for structure. +* **`Input`**: Used for single-line text entry fields (similar to standard HTML ``, used in `CreateAgent`, `AgentExecution`). +* **`Textarea`**: Used for multi-line text entry, like for the system prompt (`components/ui/textarea.tsx`, used in `CreateAgent`). +* **`Switch`**: Used for toggling options on/off, like permissions in the sandbox settings (`components/ui/switch.tsx`, used in `AgentSandboxSettings`). +* **`Label`**: Used to associate text labels with form elements (`components/ui/label.tsx`). +* **`Popover`**: Used to display floating content when a trigger is clicked (`components/ui/popover.tsx`). +* **`Toast`**: Used for temporary notification messages (`components/ui/toast.tsx`). + +You can find these components and others in the `src/components/ui/` directory. Each file defines a single, reusable UI component using React's functional component pattern, TypeScript for typing props, and Tailwind CSS classes for styling. + +For example, the `Button` component (`components/ui/button.tsx`) defines different visual `variant`s (default, destructive, outline, secondary, ghost, link) and `size`s (default, sm, lg, icon) using `class-variance-authority` and then applies the corresponding Tailwind classes (`cn` utility combines class names). When you use ``. + +## How it Works: Under the Hood (Frontend) + +The core idea behind these UI components in React is quite simple: + +1. **They are functions or classes:** A component is essentially a JavaScript/TypeScript function (or class) that receives data as `props`. +2. **They return UI:** This function returns a description of what the UI should look like (React elements, often resembling HTML). +3. **React renders the UI:** React takes this description and efficiently updates the actual web page (the Document Object Model or DOM) to match. +4. **State for interactivity:** Some components manage their own internal data called `state` (e.g., an input component's text value, whether a dialog is open). When state changes, the component re-renders. +5. **Event Handlers:** Components respond to user interactions (like clicks, typing) by calling functions defined within them or received via props (callbacks). + +The process looks like this: + +```mermaid +graph TD + A[App.tsx] --> B(Passes props like projects, callbacks like handleProjectClick) + B --> C{ProjectList Component} + C --> D(Iterates through projects, passes individual project + onProjectClick to Cards) + D --> E{Card Component (for a single project)} + E --> F(Receives project data + onProjectClick) + F -- Displays Data --> G[UI on screen (a Card)] + G -- User Clicks Card --> H(onClick handler in Card) + H --> I(Calls the onProjectClick callback received via props) + I --> J(Returns the clicked project data) + J --> C(ProjectList receives data) + C --> K(Calls the onProjectClick callback received via props) + K --> A(App.tsx receives clicked project data) + A -- Updates state (e.g., setSelectedProject) --> A + A -- Re-renders with new view --> L[New UI on screen (e.g., SessionList)] +``` + +This diagram shows the flow of data (props) and events (callbacks) that allows components to work together to create a dynamic interface. `App.tsx` is at the top, managing the main state (`view`, `selectedProject`). It passes data and functions down to its children (`ProjectList`). `ProjectList` loops and renders more children (`Card`). When a `Card` receives a user action, it calls a function passed down (`onProjectClick`), sending relevant data back up the chain, which triggers state changes in the parent (`App`), leading to a re-render and a different view being displayed. + +## Conclusion + +In this chapter, we explored Frontend UI Components, the reusable building blocks that form the visual interface of `claudia`. We learned that these components, built with React, TypeScript, and Tailwind CSS, are combined like Lego bricks to create complex views like project lists, agent managers, and the main session interface. + +We saw how components receive data through `props` and communicate back to their parents using `callbacks`. This system allows the UI to be modular, consistent, and maintainable. Understanding these components is key to seeing how `claudia` presents information and interacts with the user. + +In the next chapter, we'll bridge the gap between the frontend UI components and the backend Rust logic by learning about [Tauri Commands](04_tauri_commands_.md). These commands are the communication layer that allows the components to ask the backend for data (like listing projects) or request actions (like executing an agent). + +[Next Chapter: Tauri Commands](04_tauri_commands_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/App.tsx), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/index.ts), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/badge.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/button.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/card.tsx), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/popover.tsx), [[7]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/textarea.tsx) + +# Chapter 4: Tauri Commands + +Welcome back to the `claudia` tutorial! In [Chapter 3: Frontend UI Components](03_frontend_ui_components_.md), we explored the visual building blocks that make up `claudia`'s interface, like Buttons and Cards, and how they communicate with each other using props and callbacks. + +But those frontend components, written in TypeScript/JavaScript, can't directly talk to your operating system. They can't read files, launch other programs, or perform heavy computations safely and efficiently. This is where the backend, written in Rust, comes in. + +We need a way for the frontend UI (your browser-like window) to ask the powerful native backend to do things for it. That communication bridge is what **Tauri Commands** are all about. + +## What are Tauri Commands? + +Think of Tauri Commands as a special "phone line" or "API" that connects the frontend world (where the user clicks buttons and sees things) to the backend world (where the native code runs). + +When the user clicks a button in `claudia`'s UI, and that button needs to do something like: + +* List your projects (which requires reading the file system). +* Create a new Agent (which requires saving to a database). +* Execute a Claude Code session (which requires launching a separate process). + +...the frontend can't do this itself. Instead, it calls a specific **Tauri Command** that lives in the Rust backend. The backend command performs the requested action and then sends the result back to the frontend. + +**In simple terms:** + +* Tauri Commands are functions in the Rust backend. +* They are specifically marked so that Tauri knows they can be called from the frontend. +* The frontend calls these functions using a special `invoke` mechanism provided by Tauri. +* This allows the frontend to trigger native actions and get data from the backend. + +This separation keeps the UI responsive and safe, while the backend handles the heavy lifting and privileged operations. + +## How to Call a Tauri Command from the Frontend + +In `claudia`'s frontend (written in TypeScript), you call a backend command using the `invoke` function from the `@tauri-apps/api/core` library. + +The `invoke` function is straightforward: + +```typescript +import { invoke } from "@tauri-apps/api/core"; + +// ... later in your component or API helper ... + +async function exampleCall() { + try { + // Call the command named 'list_projects' + // If the command takes arguments, pass them as the second parameter (an object) + const result = await invoke("list_projects"); + + console.log("Projects received:", result); // Handle the result + // result will be the value returned by the Rust function + + } catch (error) { + console.error("Error calling list_projects:", error); // Handle errors + } +} + +// To actually trigger it, you might call exampleCall() in response to a button click or when a page loads. +``` + +Let's look at the `src/lib/api.ts` file, which we briefly mentioned in previous chapters. This file provides a cleaner way to call backend commands instead of using `invoke` directly everywhere. It defines functions like `listProjects`, `getProjectSessions`, `listAgents`, `createAgent`, `executeAgent`, etc., which wrap the `invoke` calls. + +Here's how the `listProjects` function is defined in `src/lib/api.ts`: + +```typescript +// src/lib/api.ts (Simplified) +import { invoke } from "@tauri-apps/api/core"; +// ... other imports and type definitions ... + +/** + * Represents a project in the ~/.claude/projects directory + */ +export interface Project { + // ... project fields ... +} + +/** + * API client for interacting with the Rust backend + */ +export const api = { + /** + * Lists all projects in the ~/.claude/projects directory + * @returns Promise resolving to an array of projects + */ + async listProjects(): Promise { // Defines a friendly TypeScript function + try { + // Calls the actual Tauri command named "list_projects" + return await invoke("list_projects"); + } catch (error) { + console.error("Failed to list projects:", error); + throw error; // Re-throw the error for the caller to handle + } + }, + + // ... other API functions like getProjectSessions, listAgents, etc. +}; +``` + +Now, in a frontend component like `ProjectList.tsx` or its parent view, instead of `invoke`, you'll see code calling `api.listProjects()`: + +```typescript +// src/components/ProjectList.tsx (Simplified - from Chapter 1) +import React, { useEffect, useState } from 'react'; +// ... other imports ... +import { api, type Project } from "@/lib/api"; // Import the api client and types + +// ... component definition ... + +export const ProjectList: React.FC = ({ onProjectClick }) => { + const [projects, setProjects] = useState([]); + // ... other state ... + + useEffect(() => { + // Fetch projects from the backend when the component loads + const loadProjects = async () => { + try { + // Call the backend command via the api helper + const projectsList = await api.listProjects(); + setProjects(projectsList); // Update the component's state with the data + } catch (err) { + console.error("Failed to load projects:", err); + } + }; + loadProjects(); // Call the function to load data + }, []); // Empty dependency array means this runs once after initial render + + // ... render function using the 'projects' state ... + // Uses projects.map to display each project (as shown in Chapter 1) +}; +``` + +This shows the typical pattern: A frontend component needs data, so it calls a function in `src/lib/api.ts` (like `api.listProjects`), which in turn uses `invoke` to call the corresponding backend command. The component then uses the received data (`projectsList`) to update its state and render the UI. + +## How to Define a Tauri Command in the Backend (Rust) + +Now, let's look at the other side: how the backend tells Tauri that a specific Rust function can be called as a command. + +This is done using the `#[tauri::command]` attribute right above the function definition. These command functions typically live in modules within the `src-tauri/src/commands/` directory (like `claude.rs` or `agents.rs`). + +Here's the simplified Rust code for the `list_projects` command, located in `src-tauri/src/commands/claude.rs`: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +use tauri::command; +use serde::{Serialize, Deserialize}; // Needed for sending data back + +// Define the structure that will be sent back to the frontend +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub id: String, + pub path: String, + pub sessions: Vec, + pub created_at: u64, +} + +// Mark this function as a Tauri command +#[command] +pub async fn list_projects() -> Result, String> { + // ... Code to find ~/.claude and read project directories ... + // This is where the file system access happens (backend logic) + + let mut projects = Vec::new(); + + // Simplified: Imagine we found some projects and populated the 'projects' vector + // For a real implementation, see the detailed code snippet in Chapter 1 + + // Example placeholder data: + projects.push(Project { + id: "encoded-path-1".to_string(), + path: "/path/to/my/project1".to_string(), + sessions: vec!["session1_id".to_string(), "session2_id".to_string()], + created_at: 1678886400, // Example timestamp + }); + projects.push(Project { + id: "encoded-path-2".to_string(), + path: "/path/to/my/project2".to_string(), + sessions: vec!["session3_id".to_string()], + created_at: 1678972800, // Example timestamp + }); + + + // Return the vector of Project structs. + // Result is often used for commands that might fail. + // Tauri automatically serializes Vec into JSON for the frontend. + Ok(projects) +} + +// ... other commands defined in this file ... +``` + +Key points here: + +1. `#[tauri::command]`: This attribute is essential. It tells Tauri to generate the necessary code to make this Rust function callable from the frontend JavaScript. +2. `pub async fn`: Commands are typically `async` functions because they often perform non-blocking operations (like reading files, launching processes) that shouldn't block the main UI thread. They must also be `pub` (public) so Tauri can access them. +3. `Result, String>`: This is the return type. `Result` is a standard Rust type for handling operations that can either succeed (`Ok`) or fail (`Err`). Here, on success, it returns a `Vec` (a list of `Project` structs); on failure, it returns a `String` error message. Tauri handles converting this Rust `Result` into a JavaScript Promise that resolves on `Ok` and rejects on `Err`. +4. `#[derive(Serialize, Deserialize)]`: Any custom data structures (like `Project` here) that you want to send between the frontend and backend must be able to be converted to/from a common format like JSON. `serde` is a Rust library for this, and deriving `Serialize` and `Deserialize` (for data going back and forth) makes this automatic. + +## Registering Commands + +Finally, for Tauri to know about your command functions, they need to be registered in the main application entry point, `src-tauri/src/main.rs`. + +In `src-tauri/src/main.rs`, there's a section using `tauri::generate_handler!` that lists all the command functions that the frontend is allowed to call: + +```rust +// src-tauri/src/main.rs (Simplified) +// ... imports ... + +mod commands; // Import your commands module + +use commands::claude::{ + list_projects, // Import the specific command functions + get_project_sessions, + // ... import other claude commands ... +}; +use commands::agents::{ + list_agents, // Import agent commands + create_agent, + execute_agent, + // ... import other agent commands ... +}; +// ... import commands from other modules like sandbox, usage, mcp ... + +fn main() { + // ... setup code ... + + tauri::Builder::default() + // ... plugins and setup ... + .invoke_handler(tauri::generate_handler![ // **This is where commands are registered!** + list_projects, // List the name of each command function + get_project_sessions, + list_agents, + create_agent, + execute_agent, + // ... list all other commands you want to expose ... + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +The `tauri::generate_handler! macro` takes a list of function names that are marked with `#[tauri::command]`. It generates the code needed for Tauri's core to receive `invoke` calls from the frontend and route them to the correct Rust function. If a command isn't listed here, the frontend can't call it. + +## How it Works: Under the Hood + +Let's visualize the flow when the frontend calls a Tauri Command. + +Imagine the user is on the Projects screen, and the `ProjectList` component needs the list of projects: + +```mermaid +sequenceDiagram + participant FrontendUI as Frontend UI (ProjectList.tsx) + participant FrontendAPI as Frontend API (api.ts) + participant TauriCore as Tauri Core + participant BackendCommands as Backend Commands (claude.rs) + participant Filesystem as Filesystem + + FrontendUI->>FrontendAPI: Need projects list + FrontendAPI->>TauriCore: invoke("list_projects") + Note over TauriCore: Tauri routes call to registered handler + TauriCore->>BackendCommands: Call list_projects() function + BackendCommands->>Filesystem: Read ~/.claude/projects + Filesystem-->>BackendCommands: Return directory contents + BackendCommands->>BackendCommands: Process data (create Project structs) + BackendCommands-->>TauriCore: Return Result, String> + TauriCore-->>FrontendAPI: Resolve invoke Promise with Vec + FrontendAPI-->>FrontendUI: Return projects data + FrontendUI->>FrontendUI: Update state with projects data + FrontendUI->>FrontendUI: Render UI (display projects) +``` + +1. The `ProjectList` component (Frontend UI) decides it needs the list of projects, perhaps in a `useEffect` hook when it mounts. +2. It calls `api.listProjects()` (Frontend API wrapper). +3. `api.listProjects()` calls `invoke("list_projects")`, which sends a message to the Tauri Core. +4. The Tauri Core receives the message "call command 'list\_projects'" and looks up the corresponding registered Rust function. +5. The Tauri Core executes the `list_projects()` function in the Backend Commands module. +6. The Rust function performs its logic, which involves interacting with the Filesystem (reading directories and files). +7. The Filesystem returns the necessary data to the Rust function. +8. The Rust function processes this data and constructs the `Vec` result. +9. The Rust function returns the `Result, String>`. Tauri automatically serializes the `Vec` into JSON. +10. The Tauri Core receives the result and sends it back to the frontend process. +11. The Promise returned by the initial `invoke` call in `api.ts` resolves with the JSON data, which Tauri automatically deserializes back into a TypeScript `Project[]` array. +12. `api.listProjects()` returns this array to the `ProjectList` component. +13. The `ProjectList` component updates its internal state, triggering React to re-render the component, displaying the list of projects on the screen. + +This same pattern is used for almost all interactions where the frontend needs to get information or trigger actions in the backend. For example, when you click "Execute" for an Agent (as seen in Chapter 2), the `AgentExecution.tsx` component calls `api.executeAgent()`, which calls the backend `execute_agent` command, which then launches the `claude` binary (as we'll see in [Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). + +## Conclusion + +In this chapter, we learned about **Tauri Commands**, the essential communication layer that bridges the gap between the frontend UI (built with React/TypeScript) and the native backend logic (written in Rust). + +We saw how the frontend uses `invoke` (often wrapped by helpful functions in `src/lib/api.ts`) to call named backend commands, passing arguments and receiving results via Promises. We also saw how backend Rust functions are defined using `#[tauri::command]`, must be `pub async fn`, return a `Result`, and how data is serialized using `serde`. Finally, we looked at how these commands are registered in `src-tauri/src/main.rs` using `tauri::generate_handler!`. + +Understanding Tauri Commands is crucial because they are the fundamental way `claudia`'s UI interacts with the powerful, native capabilities provided by the Rust backend. This mechanism allows the frontend to stay focused on presentation while relying on the backend for tasks like file system access, process management, and database interaction. + +In the next chapter, we'll delve into the very core of `claudia`'s function: how it interacts with the command-line `claude` binary to run sessions and execute tasks. + +[Next Chapter: Claude CLI Interaction](05_claude_cli_interaction_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/mod.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/lib.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/main.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/lib/api.ts) +# Chapter 5: Claude CLI Interaction + +Welcome back to the `claudia` tutorial! In our previous chapters, we learned about managing your work with Claude Code through [Session/Project Management](01_session_project_management_.md), creating specialized [Agents](02_agents_.md) to define how Claude should behave, how the [Frontend UI Components](03_frontend_ui_components_.md) like buttons and lists build the interface, and how [Tauri Commands](04_tauri_commands_.md) allow the frontend (TypeScript/React) to talk to the backend (Rust). + +Now, let's dive into the core action: how `claudia` actually makes the powerful `claude` command-line tool run and communicate with it. This chapter is all about the **Claude CLI Interaction** layer. + +## The Problem: GUI Needs to Talk to CLI + +You're using `claudia`, which is a beautiful graphical application. You click buttons, type in text boxes, and see output in a nice interface. But the actual intelligence, the part that runs your requests and generates code or text, is the `claude` command-line interface (CLI) tool that you installed separately. + +So, how does `claudia`'s backend, written in Rust, tell the `claude` CLI, which is a separate program running on your computer, what to do? How does it get the response back in real-time to show you? + +This is exactly what the Claude CLI Interaction part of `claudia` handles. It's the bridge between the graphical application and the underlying CLI tool. + +Imagine you're the director of an orchestra (`claudia`). You have a conductor's stand (the UI), but the music is played by the musicians (`claude`). You need a way to signal to the musicians what piece to play, at what tempo, and capture their performance to share with the audience. `claudia`'s CLI Interaction is your way of signaling to the `claude` process and listening to its "music" (the output). + +## What the Claude CLI Interaction Does + +The core function of this layer in `claudia`'s backend is to: + +1. **Find the `claude` binary:** Figure out where the `claude` executable is located on your system. +2. **Prepare the command:** Build the command line that needs to be run, including the `claude` binary path and all the necessary arguments (like the prompt, model, system prompt, etc.). +3. **Spawn the process:** Start the `claude` binary as a separate process. +4. **Control the environment:** Set the working directory for the `claude` process (the project path) and potentially adjust its environment variables (like the PATH). +5. **Manage sandboxing (Optional but important):** If sandboxing is enabled, ensure the `claude` process runs within the defined security restrictions (more on this in [Chapter 6: Sandboxing](06_sandboxing_.md)). +6. **Capture output:** Get the standard output (stdout) and standard error (stderr) streams from the running `claude` process in real-time. +7. **Process output:** Take the raw output (which is in a special JSONL format for Claude Code) and process it. +8. **Report status/output:** Send the processed output and status updates (running, complete, failed) back to the frontend so the user interface can update. +9. **Manage process lifecycle:** Keep track of the running process and handle requests to stop or kill it. + +## Triggering a Claude Code Run from the Frontend + +You've already seen in Chapter 4 how frontend components use `api` functions to call backend commands. This is how you initiate a Claude Code run. + +Whether you're executing an Agent (from `AgentExecution.tsx`) or starting/continuing a direct session (from `ClaudeCodeSession.tsx`), the frontend makes a call to a specific backend command responsible for launching `claude`. + +Here's a simplified look at how `AgentExecution.tsx` initiates a run: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +import { api, type Agent } from "@/lib/api"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; + +// ... component definition ... + +const handleExecute = async () => { + // ... validation and state updates (isLoading, etc.) ... + + try { + // Set up event listeners first (covered in Chapter 7) + // These listeners will receive output and status updates from the backend + const outputUnlisten = await listen("agent-output", (event) => { + // Process received output line (JSONL) + // ... update messages state ... + }); + const completeUnlisten = await listen("agent-complete", (event) => { + // Process completion status + // ... update isRunning state ... + }); + // ... store unlisten functions ... + + // Call the backend command to execute the agent + // This command prepares and spawns the 'claude' process + await api.executeAgent(agent.id!, projectPath, task, model); + + } catch (err) { + console.error("Failed to execute agent:", err); + // ... handle error ... + } +}; + +// ... render function with button calling handleExecute ... +``` + +And here's a similar pattern from `ClaudeCodeSession.tsx` for starting a new session: + +```typescript +// src/components/ClaudeCodeSession.tsx (Simplified) +// ... imports ... +import { api, type Session } from "@/lib/api"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; + +// ... component definition ... + +const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => { + // ... validation and state updates (isLoading, etc.) ... + + try { + // Add the user message to the UI immediately + // ... update messages state ... + + // Clean up old listeners, set up new ones (for "claude-output", "claude-complete") + // ... setup listeners (covered in Chapter 7) ... + + // Call the appropriate backend command + // This command prepares and spawns the 'claude' process + if (isFirstPrompt && !session) { + await api.executeClaudeCode(projectPath, prompt, model); // New session + } else if (session && isFirstPrompt) { + await api.resumeClaudeCode(projectPath, session.id, prompt, model); // Resume session + } else { + await api.continueClaudeCode(projectPath, prompt, model); // Continue conversation + } + + } catch (err) { + console.error("Failed to send prompt:", err); + // ... handle error ... + } +}; + +// ... render function with FloatingPromptInput component calling handleSendPrompt ... +``` + +These snippets show that from the frontend's perspective, starting a Claude Code interaction is simply calling a backend API function (a Tauri Command wrapper) and then listening for events that the backend sends back as the process runs and finishes. + +## How it Works: Under the Hood (Backend) + +When the backend receives a Tauri command like `execute_agent` or `execute_claude_code`, it performs a series of steps to launch and manage the `claude` process. + +Here's a simplified step-by-step flow: + +1. **Find the `claude` executable:** The backend needs the full path to the `claude` binary. It looks in common installation locations and potentially a path saved in `claudia`'s settings. +2. **Determine process parameters:** It gathers the necessary information for the command: the prompt (`-p`), the system prompt (`--system-prompt`, from the Agent config or CLAUDE.md), the model (`--model`), the output format (`--output-format stream-json` is crucial for real-time processing), flags like `--verbose` and `--dangerously-skip-permissions` (since `claudia` handles permissions via sandboxing), and the working directory (`--current-dir` or set via `Command`). +3. **Prepare Sandbox (if enabled):** Based on Agent permissions or global settings, the backend constructs sandbox rules using the `gaol` library. This involves defining what file paths (`file_read_all`, `file_write_all`) and network connections (`network_outbound`) the `claude` process is allowed to make. This is tightly linked to the actual command execution. +4. **Build the Command object:** Rust's standard library (and the `tokio` library for asynchronous operations) provides a `Command` struct to build process commands. The backend creates a `Command` instance, sets the `claude` binary path, adds all the arguments, sets the working directory (`current_dir`), and configures standard input/output (`stdin`, `stdout`, `stderr`) to be piped so the backend can capture them. +5. **Spawn the child process:** The `Command` object is executed using a method like `spawn()`. This starts the `claude` process and gives the backend a handle to it (a `Child` object). +6. **Capture Output Streams:** The `stdout` and `stderr` streams of the child process, which were configured to be piped, are now available as asynchronous readers. The backend spawns separate asynchronous tasks (using `tokio::spawn`) to continuously read lines from these streams. +7. **Process and Emit:** As each line of output (usually a JSON object in the JSONL format) or error arrives, the reading tasks process it (e.g., parse JSON, extract relevant data) and immediately emit it as a Tauri event back to the frontend (`agent-output`, `claude-output`, `agent-error`, `claude-error`). This provides the real-time streaming experience. +8. **Monitor Completion:** The backend also has a task that waits for the `claude` process to finish (`child.wait().await`). When it exits, the task notifies the frontend (e.g., via `agent-complete`, `claude-complete`) and potentially updates internal state or a database record (like the `agent_runs` table for Agents). +9. **Handle Cancellation:** If the user requests to stop the process (e.g., clicking a "Stop" button for an Agent run), the backend uses the process ID (PID) to send a termination signal (`kill`). + +Here's a sequence diagram showing the flow for a standard `execute_claude_code` call: + +```mermaid +sequenceDiagram + participant FrontendUI as Frontend UI (ClaudeCodeSession.tsx) + participant FrontendAPI as Frontend API (api.ts) + participant TauriCore as Tauri Core + participant BackendCommands as Backend Commands (claude.rs) + participant OS as Operating System + participant ClaudeCLI as claude binary + + FrontendUI->>FrontendAPI: User submits prompt (call executeClaudeCode) + FrontendAPI->>TauriCore: invoke("execute_claude_code", { prompt, path, model }) + Note over TauriCore: Tauri routes call + TauriCore->>BackendCommands: Call execute_claude_code() + BackendCommands->>BackendCommands: Find claude binary path (find_claude_binary) + BackendCommands->>BackendCommands: Prepare Command object (args, cwd, piped streams) + BackendCommands->>OS: Spawn process (claude binary) + OS-->>BackendCommands: Return Child process handle + BackendCommands->>BackendCommands: Spawn tasks to read stdout/stderr + loop While ClaudeCLI is running & produces output + ClaudeCLI-->>OS: Write to stdout/stderr pipe + OS-->>BackendCommands: Data available in pipe + BackendCommands->>BackendCommands: Read & process output line + BackendCommands->>TauriCore: Emit "claude-output" or "claude-error" event + TauriCore-->>FrontendUI: Receive event data + FrontendUI->>FrontendUI: Display output line in UI + end + ClaudeCLI-->>OS: Process exits + OS-->>BackendCommands: Process termination status + BackendCommands->>BackendCommands: Task waits for process exit + BackendCommands->>TauriCore: Emit "claude-complete" event + TauriCore-->>FrontendUI: Receive event + FrontendUI->>FrontendUI: Update UI (execution finished) +``` + +This diagram visually outlines how the request flows from the frontend to the backend, how the backend launches the separate `claude` process via the OS, how output streams back through the backend and Tauri, and finally how the frontend is updated in real-time. + +## Diving into the Backend Code + +Let's look at some key parts of the Rust code in `src-tauri/src/commands/claude.rs` and `src-tauri/src/commands/agents.rs` that handle this process interaction. + +First, finding the binary and setting up the environment: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::process::Command; +use std::process::Stdio; +use tauri::{AppHandle, Emitter, Manager}; + +// This function tries to locate the 'claude' executable +fn find_claude_binary(app_handle: &AppHandle) -> Result { + // ... logic to check settings, common paths, 'which' command ... + // Returns the found path or an error + Ok("path/to/claude".to_string()) // Simplified +} + +// This function creates a Tokio Command object, setting environment variables +fn create_command_with_env(program: &str) -> Command { + let mut cmd = Command::new(program); + + // Inherit essential environment variables like PATH, HOME, etc. + // This helps the 'claude' binary find Node.js and other dependencies + for (key, value) in std::env::vars() { + // ... filtering for safe/necessary variables ... + cmd.env(&key, &value); + } + + cmd // Return the Command object +} + +// ... rest of the file ... +``` + +`find_claude_binary` is crucial to ensure `claudia` can actually find the executable regardless of how it was installed. `create_command_with_env` is a helper to build the base command object and ensure it inherits essential environment variables, which is often necessary for `claude` to run correctly, especially on macOS GUI launches where the default PATH is minimal. + +Next, the core logic for spawning the process and handling its output streams. This is extracted into a helper function `spawn_claude_process` used by `execute_claude_code`, `continue_claude_code`, and `resume_claude_code`. A similar pattern exists within `execute_agent`. + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::io::{AsyncBufReadExt, BufReader}; + +/// Helper function to spawn Claude process and handle streaming +async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> { + log::info!("Spawning Claude process..."); + + // Configure stdout and stderr to be piped + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + // Spawn the process asynchronously + let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?; + + log::info!("Claude process spawned successfully with PID: {:?}", child.id()); + + // Take the piped stdout and stderr handles + let stdout = child.stdout.take().ok_or("Failed to get stdout")?; + let stderr = child.stderr.take().ok_or("Failed to get stderr")?; + + // Create asynchronous buffered readers for the streams + let stdout_reader = BufReader::new(stdout); + let stderr_reader = BufReader::new(stderr); + + // Spawn a separate task to read and process stdout lines + let app_handle_stdout = app.clone(); + tokio::spawn(async move { + let mut lines = stdout_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + log::debug!("Claude stdout: {}", line); + // Emit the line as an event to the frontend + // Frontend listens for "claude-output" + let _ = app_handle_stdout.emit("claude-output", &line); + } + log::info!("Finished reading Claude stdout."); + }); + + // Spawn a separate task to read and process stderr lines + let app_handle_stderr = app.clone(); + tokio::spawn(async move { + let mut lines = stderr_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + log::error!("Claude stderr: {}", line); + // Emit error lines as an event to the frontend + // Frontend listens for "claude-error" + let _ = app_handle_stderr.emit("claude-error", &line); + } + log::info!("Finished reading Claude stderr."); + }); + + // Spawn a task to wait for the process to finish + let app_handle_complete = app.clone(); + tokio::spawn(async move { + match child.wait().await { // Wait for the process to exit + Ok(status) => { + log::info!("Claude process exited with status: {}", status); + // Emit a completion event to the frontend + // Frontend listens for "claude-complete" + tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Small delay + let _ = app_handle_complete.emit("claude-complete", status.success()); + } + Err(e) => { + log::error!("Failed to wait for Claude process: {}", e); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Small delay + let _ = app_handle_complete.emit("claude-complete", false); // Indicate failure + } + } + }); + + Ok(()) +} + +// ... rest of the file with commands like execute_claude_code calling spawn_claude_process ... +``` + +This `spawn_claude_process` function is the heart of the interaction. It sets up the communication channels (`stdout`, `stderr` pipes), starts the `claude` process, and then uses `tokio::spawn` to run multiple things concurrently: reading output, reading errors, and waiting for the process to finish. Each piece of output or status change triggers an `app.emit` call, sending the information via Tauri's event system back to the frontend. + +Finally, handling cancellation for Agent runs involves finding the process ID (PID) and sending a signal. + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +// ... imports ... +use rusqlite::{params, Connection, Result as SqliteResult}; // For database access + +/// Kill a running agent session +#[tauri::command] +pub async fn kill_agent_session( + db: State<'_, AgentDb>, // Access to the database state + run_id: i64, +) -> Result { + log::info!("Attempting to kill agent session run: {}", run_id); + + let pid_result = { + let conn = db.0.lock().map_err(|e| e.to_string())?; + // Retrieve the PID from the database for the specific run + conn.query_row( + "SELECT pid FROM agent_runs WHERE id = ?1 AND status = 'running'", + params![run_id], + |row| row.get::<_, Option>(0) + ) + .map_err(|e| e.to_string())? + }; + + if let Some(pid) = pid_result { + log::info!("Found PID {} for run {}", pid, run_id); + // Use the standard library to send a kill signal + // Behavior differs slightly on Windows vs Unix-like systems + let kill_result = if cfg!(target_os = "windows") { + std::process::Command::new("taskkill") + .args(["/F", "/PID", &pid.to_string()]) // Force kill by PID + .output() + } else { + std::process::Command::new("kill") + .args(["-TERM", &pid.to_string()]) // Send termination signal + .output() + }; + + match kill_result { + Ok(output) if output.status.success() => { + log::info!("Successfully sent kill signal to process {}", pid); + } + Ok(_) => { + log::warn!("Kill command failed for PID {}", pid); + } + Err(e) => { + log::warn!("Failed to execute kill command for PID {}: {}", pid, e); + } + } + } else { + log::warn!("No running PID found for run {}", run_id); + } + + // Update the database to mark the run as cancelled, regardless of kill success + let conn = db.0.lock().map_err(|e| e.to_string())?; + let updated = conn.execute( + "UPDATE agent_runs SET status = 'cancelled', completed_at = CURRENT_TIMESTAMP WHERE id = ?1 AND status = 'running'", + params![run_id], + ).map_err(|e| e.to_string())?; + + Ok(updated > 0) // Return true if a record was updated +} + +// ... rest of the file ... +``` + +This `kill_agent_session` command looks up the process ID associated with the agent run in the database, then attempts to terminate that process using system commands (`kill` or `taskkill`). Finally, it updates the database record for the run to mark it as "cancelled". + +## Conclusion + +In this chapter, we explored the **Claude CLI Interaction** layer, which is fundamental to how `claudia` functions. We learned that this part of the backend is responsible for finding the `claude` binary, preparing the command with all necessary arguments, spawning the `claude` process, setting its environment (including sandboxing), capturing its output and errors in real-time, and managing its lifecycle until completion or cancellation. + +We saw how frontend calls to Tauri Commands trigger this process, how the backend uses Rust's `Command` features and `tokio` for asynchronous stream handling, and how output and status updates are sent back to the frontend via Tauri events, enabling the real-time display of results. This interaction layer effectively turns the `claude` CLI into a powerful engine driven by the user-friendly `claudia` graphical interface. + +Next, we'll take a closer look at a critical aspect touched upon in this chapter: **Sandboxing**. We'll see how `claudia` uses operating system features to limit the permissions of the `claude` process, enhancing security when running code or interacting with your file system. + +[Next Chapter: Sandboxing](06_sandboxing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/claude.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/executor.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx) diff --git a/Claudia-docs/V_1/claudia_2.md b/Claudia-docs/V_1/claudia_2.md new file mode 100644 index 00000000..a2a56c32 --- /dev/null +++ b/Claudia-docs/V_1/claudia_2.md @@ -0,0 +1,2500 @@ +# Chapter 6: Sandboxing + +Welcome back to the `claudia` tutorial! In our previous chapters, we've learned about organizing your work with [Session/Project Management](01_session_project_management_.md), defining specialized assistants with [Agents](02_agents_.md), how the [Frontend UI Components](03_frontend_ui_components_.md) create the user interface, how [Tauri Commands](04_tauri_commands_.md) connect the frontend and backend, and how `claudia` interacts with the `claude` command-line tool in [Claude CLI Interaction](05_claude_cli_interaction_.md). + +Now, let's talk about a crucial aspect of security: **Sandboxing**. + +## The Problem: Running Untrusted Code + +When you use `claudia` to run an Agent or a direct Claude Code session, you are essentially asking the application to launch the separate `claude` binary on your computer. This `claude` binary can then execute code or perform actions based on the instructions it receives from Claude (and indirectly, from you). + +Imagine you ask Claude to "write a script to delete all files in `/tmp`". While this is a harmless directory, what if you accidentally asked it to delete files in your `/Users/yourname/Documents` folder, or worse, system files? Or what if a malicious instruction somehow slipped into the context? + +Running external processes, especially ones that might execute code or interact with your file system and network, introduces a security risk. By default, any program you run has the same permissions as you do. It could potentially read your sensitive files, delete important data, or connect to unwanted places on the internet. + +This is where **Sandboxing** comes in. + +## What is Sandboxing? + +Sandboxing is like putting a protective barrier around the process that `claudia` launches (the `claude` binary). It creates a restricted environment that limits what that process can see and do on your computer, based on a predefined set of rules. + +Think of it like giving the AI a restricted workspace. You give it access only to the specific tools and areas it needs to do its job for this particular task, and nothing more. + +In `claudia`, sandboxing is primarily used to control the `claude` process's access to: + +1. **File System:** Prevent reading or writing files outside of specific allowed directories (like your project folder). +2. **Network:** Prevent making unwanted connections to the internet or local network. +3. **System Information:** Limit access to potentially sensitive system details. + +By default, `claudia` aims to run Agents and sessions within a sandbox, giving you control over their permissions. + +## Sandboxing with Agents + +The primary way you interact with sandboxing settings in `claudia` is through the **Agent configuration**. As you saw in [Chapter 2: Agents](02_agents_.md), each Agent has specific permission toggles. + +Let's revisit the simplified `AgentSandboxSettings.tsx` component from Chapter 2: + +```typescript +// src/components/AgentSandboxSettings.tsx (Simplified) +// ... imports ... +import { Switch } from "@/components/ui/switch"; +// ... other components ... + +export const AgentSandboxSettings: React.FC = ({ + agent, + onUpdate, + className +}) => { + // ... handleToggle function ... + + return ( + // ... Card and layout ... + {/* Master sandbox toggle */} +
+ + handleToggle('sandbox_enabled', checked)} + /> +
+ + {/* Permission toggles - conditional render */} + {agent.sandbox_enabled && ( +
+ {/* File Read Toggle */} +
+ + handleToggle('enable_file_read', checked)} + /> +
+ {/* File Write Toggle */} +
+ + handleToggle('enable_file_write', checked)} + /> +
+ {/* Network Toggle */} +
+ + handleToggle('enable_network', checked)} + /> +
+
+ )} + {/* ... Warning when sandbox disabled ... */} + // ... end Card ... + ); +}; +``` + +These switches directly control whether the `claude` process launched *by this specific Agent* will be sandboxed and what high-level permissions it will have: + +* **Enable Sandbox:** The main switch. If off, sandboxing is disabled for this Agent, and the process runs with full permissions (like running `claude` directly in your terminal). This should be used with caution. +* **File Read Access:** If enabled, the sandboxed process can read files. Without this, it might not even be able to read the source files in your project directory. +* **File Write Access:** If enabled, the sandboxed process can create or modify files. +* **Network Access:** If enabled, the sandboxed process can make outbound network connections (e.g., accessing APIs, cloning repositories). + +These Agent-specific toggles allow you to quickly define a security posture tailored to the Agent's purpose. A "Code Reader" Agent might only need File Read. A "Code Fixer" might need File Read and Write. A "Web API Helper" might need Network Access. + +## How it Works: Under the Hood + +When you click "Execute" for an Agent or start a session, `claudia`'s backend takes the Agent's sandbox settings (or default settings for direct sessions) and translates them into concrete rules that the operating system can enforce. + +`claudia` uses system-level sandboxing mechanisms through a library called `gaol`. `gaol` provides a way for the parent process (`claudia`'s backend) to define restrictions for a child process (`claude`). + +Here's a simplified look at the steps when `claudia` launches a sandboxed `claude` process: + +1. **Get Agent Permissions:** The backend fetches the selected Agent's configuration from the database, including the `sandbox_enabled`, `enable_file_read`, `enable_file_write`, and `enable_network` fields. +2. **Load Sandbox Profile & Rules:** `claudia` stores more detailed, reusable sandbox configurations called "Profiles" and "Rules" in its database ([Chapter 2: Agents](02_agents_.md)). The Agent might be linked to a specific Profile, or a default Profile is used. The backend loads the rules associated with this Profile. +3. **Combine Agent Permissions and Rules:** The backend logic combines the high-level Agent toggles with the detailed Profile rules. For example, if the Agent has `enable_file_read: false`, any "file read" rules from the loaded Profile are ignored for this run. If `enable_file_read: true`, the specific paths defined in the Profile rules (like "allow reading subpaths of the project directory") are used. The project path itself (from [Chapter 1: Session/Project Management](01_session_project_management_.md)) is crucial here, as file access is often restricted to this directory. +4. **Build `gaol` Profile:** The combined set of effective rules is used to build a `gaol::profile::Profile` object in memory. This object contains the precise operations the child process will be allowed or denied. +5. **Prepare & Spawn Command:** The backend prepares the command to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). It configures the command to run within the sandbox environment defined by the `gaol` Profile. This might involve setting special environment variables or using `gaol`'s API to spawn the child process with the restrictions already applied by the parent. +6. **OS Enforces Sandbox:** When the `claude` process starts, the operating system, guided by the `gaol` library and the configured profile, actively monitors the process. If the `claude` process attempts an action that is *not* allowed by the sandbox rules (like trying to read a file outside the permitted paths when file read is enabled, or any file if file read is disabled), the operating system blocks the action immediately. +7. **Violation Logging:** If a sandboxed process attempts a forbidden action, `claudia` can detect this violation and log it to its database. This helps you understand if an Agent is trying to do something unexpected. + +Here's a simplified sequence diagram illustrating the sandboxing flow during execution: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Database as agents.db + participant SandboxLogic as Sandbox Module (Rust) + participant OS as Operating System + participant ClaudeCLI as claude binary + + Frontend->>Backend: Call execute_agent(...) + Backend->>Database: Get Agent Config (incl. permissions) + Database-->>Backend: Agent Config + Backend->>Database: Get Sandbox Profile & Rules + Database-->>Backend: Profile & Rules + Backend->>SandboxLogic: Combine Agent Permissions & Rules + SandboxLogic->>SandboxLogic: Build gaol::Profile + SandboxLogic-->>Backend: gaol::Profile ready + Backend->>OS: Spawn claude process (with gaol::Profile / env) + OS-->>Backend: Process Handle, PID + Note over OS,ClaudeCLI: OS enforces sandbox rules + ClaudeCLI->>OS: Attempt operation (e.g., read file) + alt Operation Allowed + OS-->>ClaudeCLI: Operation succeeds + else Operation Denied (Violation) + OS-->>ClaudeCLI: Operation fails (Permission denied) + Note over OS: Violation detected + OS->>SandboxLogic: Notify of violation (if configured) + SandboxLogic->>Database: Log Violation + end + ClaudeCLI-->>OS: Process exits + OS-->>Backend: Process status + Backend->>Frontend: Notify completion/output +``` + +This diagram shows how the Agent's settings propagate through the backend to influence the creation of the sandbox profile, which is then enforced by the operating system when the `claude` process is launched. + +## Diving into the Backend Code + +Let's look at snippets from the Rust code related to sandboxing, found primarily in the `src-tauri/src/sandbox/` module and `src-tauri/src/commands/sandbox.rs`. + +The `Agent` struct (from `src-tauri/src/commands/agents.rs`) holds the basic toggles: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Agent { + // ... other fields ... + pub sandbox_enabled: bool, + pub enable_file_read: bool, + pub enable_file_write: bool, // Note: This permission is often difficult to enforce precisely via sandboxing alone and might require manual user confirmation or is inherently less secure. + pub enable_network: bool, + // ... other fields ... +} +``` + +The `src-tauri/src/commands/sandbox.rs` file contains Tauri commands for managing sandbox profiles and rules stored in the database, and for viewing violations: + +```rust +// src-tauri/src/commands/sandbox.rs (Simplified) +// ... imports ... + +// Represents a detailed rule in a sandbox profile +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SandboxRule { + pub id: Option, + pub profile_id: i64, // Links to a profile + pub operation_type: String, // e.g., "file_read_all", "network_outbound" + pub pattern_type: String, // e.g., "subpath", "literal" + pub pattern_value: String, // e.g., "{{PROJECT_PATH}}", "/home/user/.config" + pub enabled: bool, + pub platform_support: Option, // e.g., "[\"macos\", \"linux\"]" + pub created_at: String, +} + +// Represents a log entry for a denied operation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SandboxViolation { + pub id: Option, + pub profile_id: Option, // What profile was active? + pub agent_id: Option, // What agent was running? + pub agent_run_id: Option, // What specific run? + pub operation_type: String, // What was attempted? + pub pattern_value: Option, // What path/address was involved? + pub process_name: Option, // Which binary? + pub pid: Option, // Which process ID? + pub denied_at: String, // When did it happen? +} + +// Tauri command to list sandbox profiles +#[tauri::command] +pub async fn list_sandbox_profiles(/* ... */) -> Result, String> { /* ... */ } + +// Tauri command to list rules for a profile +#[tauri::command] +pub async fn list_sandbox_rules(/* ... */) -> Result, String> { /* ... */ } + +// Tauri command to view recorded violations +#[tauri::command] +pub async fn list_sandbox_violations(/* ... */) -> Result, String> { /* ... */ } + +// ... other commands for creating/updating/deleting profiles and rules ... +``` + +These commands allow the frontend to manage the detailed sandbox configurations that underpin the Agent's simpler toggles. For example, when you enable "File Read Access" on an Agent, the backend loads rules of `operation_type: "file_read_all"` from the selected profile. + +The logic to combine Agent permissions, Profile rules, and build the `gaol::profile::Profile` happens in the `src-tauri/src/sandbox/profile.rs` and `src-tauri/src/sandbox/executor.rs` modules. + +The `ProfileBuilder` is used to translate `SandboxRule` database entries into `gaol::profile::Operation` objects: + +```rust +// src-tauri/src/sandbox/profile.rs (Simplified) +// ... imports ... +use gaol::profile::{Operation, PathPattern, AddressPattern, Profile}; +// ... SandboxRule struct ... + +pub struct ProfileBuilder { + project_path: PathBuf, // The current project directory + home_dir: PathBuf, // The user's home directory +} + +impl ProfileBuilder { + // ... constructor ... + + /// Build a gaol Profile from database rules, filtered by agent permissions + pub fn build_agent_profile(&self, rules: Vec, sandbox_enabled: bool, enable_file_read: bool, enable_file_write: bool, enable_network: bool) -> Result { + // If sandbox is disabled, return empty profile (no restrictions) + if !sandbox_enabled { + // ... create and return empty profile ... + } + + let mut effective_rules = Vec::new(); + + for rule in rules { + if !rule.enabled { continue; } + + // Filter rules based on Agent permissions: + let include_rule = match rule.operation_type.as_str() { + "file_read_all" | "file_read_metadata" => enable_file_read, + "network_outbound" => enable_network, + "system_info_read" => true, // System info often needed, allow if sandbox is ON + _ => true // Default to include if unknown + }; + + if include_rule { + effective_rules.push(rule); + } + } + + // Always ensure project path access is included if file read is ON + if enable_file_read { + // ... add rule for project path if not already present ... + } + + // Now build the actual gaol Profile from the effective rules + self.build_profile_with_serialization(effective_rules) // This translates rules into gaol::Operation + } + + /// Translates SandboxRules into gaol::Operation and serialized form + fn build_operation_with_serialization(&self, rule: &SandboxRule) -> Result> { + match rule.operation_type.as_str() { + "file_read_all" => { + let (pattern, path, is_subpath) = self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?; + Ok(Some((Operation::FileReadAll(pattern), SerializedOperation::FileReadAll { path, is_subpath }))) + }, + "network_outbound" => { + let (pattern, serialized) = self.build_address_pattern_with_serialization(&rule.pattern_type, &rule.pattern_value)?; + Ok(Some((Operation::NetworkOutbound(pattern), serialized))) + }, + // ... handle other operation types ... + _ => Ok(None) + } + } + + // ... helper functions to build path/address patterns ... +} +``` + +The `build_agent_profile` function is key. It takes the raw rules from the database and the Agent's simple boolean toggles, then filters the rules. It also ensures essential access (like reading the project directory) is granted if file read is enabled. Finally, it calls `build_profile_with_serialization` to create the actual `gaol::Profile` object and a simplified, serializable representation of the rules (`SerializedProfile`). + +This `SerializedProfile` is then passed to the `SandboxExecutor`: + +```rust +// src-tauri/src/sandbox/executor.rs (Simplified) +// ... imports ... +use gaol::sandbox::Sandbox; +use tokio::process::Command; +use std::path::Path; + +pub struct SandboxExecutor { + profile: gaol::profile::Profile, // The gaol profile object + project_path: PathBuf, + serialized_profile: Option, // Serialized rules for child process +} + +impl SandboxExecutor { + // ... constructor ... + + /// Prepare a tokio Command for sandboxed execution + /// The sandbox will be activated in the child process by reading environment variables + pub fn prepare_sandboxed_command(&self, command: &str, args: &[&str], cwd: &Path) -> Command { + let mut cmd = Command::new(command); + cmd.args(args).current_dir(cwd); + + // ... inherit environment variables like PATH, HOME ... + + // Serialize the sandbox rules and set environment variables + if let Some(ref serialized) = self.serialized_profile { + let rules_json = serde_json::to_string(serialized).expect("Failed to serialize rules"); + // NOTE: These environment variables are currently commented out in the actual code + // for debugging and compatibility reasons. + // In a fully enabled child-side sandboxing model, these would be set: + // cmd.env("GAOL_SANDBOX_ACTIVE", "1"); + // cmd.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref()); + // cmd.env("GAOL_SANDBOX_RULES", &rules_json); + log::warn!("🚨 Sandboxing environment variables for child process are currently disabled!"); + } else { + log::warn!("🚨 No serialized profile - running without sandbox environment!"); + } + + cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()) + } + + // ... Other execution methods ... +} +``` + +The `prepare_sandboxed_command` function takes the `gaol::profile::Profile` and the `SerializedProfile`. Although the environment variable mechanism shown above is temporarily disabled in the provided code snippets, the *intention* is for the parent process (`claudia`'s backend) to set up the environment for the child process (`claude`). The child process, if it supports this model (like `gaol`'s `ChildSandbox::activate()`), would read these environment variables upon startup and activate the sandbox *within itself* before executing the main task. + +Alternatively, `gaol` also supports launching the child process directly from the sandboxed parent using `Sandbox::start()`. The provided code attempts this first but falls back due to current `gaol` library limitations regarding getting the child process handle back. + +The `src-tauri/src/sandbox/platform.rs` file defines what kind of sandboxing capabilities are available and supported on the current operating system (Linux, macOS, FreeBSD have some support). + +```rust +// src-tauri/src/sandbox/platform.rs (Simplified) +// ... imports ... + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlatformCapabilities { + pub os: String, + pub sandboxing_supported: bool, // Is sandboxing generally possible? + // ... details about specific operation support ... +} + +pub fn get_platform_capabilities() -> PlatformCapabilities { /* ... detects OS and returns capabilities ... */ } +pub fn is_sandboxing_available() -> bool { /* ... checks if OS is supported ... */ } +``` + +This is used by the UI (via the `get_platform_capabilities` command) to inform the user if sandboxing is fully supported or if there are limitations on their platform. + +In summary, sandboxing in `claudia` works by: +1. Allowing users to set high-level permissions (read/write/network) on Agents via the UI. +2. Storing detailed, reusable sandbox Profiles and Rules in the backend database. +3. Combining Agent permissions with Profile rules in the backend to create a specific set of restrictions for a given process run. +4. Using system-level sandboxing features (via the `gaol` library and potentially environment variables) to apply these restrictions when launching the `claude` process. +5. Logging any attempts by the sandboxed process to violate these rules. + +This multi-layered approach provides both ease of use (Agent toggles) and flexibility (detailed rules in Profiles), significantly improving security when running AI-generated instructions or code. + +## Conclusion + +In this chapter, we explored **Sandboxing**, `claudia`'s security system. We learned why running external processes requires security measures and how sandboxing provides a protective barrier to limit what the `claude` process can access or do. + +We saw how you control sandboxing primarily through Agent permissions in the UI, enabling or disabling file read, file write, and network access. We then dived into the backend to understand how these simple toggles are combined with detailed Sandbox Profile rules to build a concrete `gaol::profile::Profile`. This profile is then used to launch the `claude` binary within a restricted environment enforced by the operating system, with potential violations being logged. + +Understanding sandboxing is key to securely leveraging the power of Claude Code, especially when it interacts with your local file system. + +In the next chapter, we'll learn how `claudia` handles the continuous stream of output from the `claude` binary to update the UI in real-time: [Streamed Output Processing](07_streamed_output_processing_.md). + +[Next Chapter: Streamed Output Processing](07_streamed_output_processing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/sandbox.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/executor.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/mod.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/platform.rs), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/profile.rs), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentSandboxSettings.tsx) + +# Chapter 7: Streamed Output Processing + +Welcome back to the `claudia` tutorial! In our previous chapters, we've learned about organizing your work with [Session/Project Management](01_session_project_management_.md), defining specialized assistants with [Agents](02_agents_.md), how the [Frontend UI Components](03_frontend_ui_components_.md) create the user interface, how [Tauri Commands](04_tauri_commands_.md) connect the frontend and backend, how `claudia` interacts with the `claude` command-line tool in [Claude CLI Interaction](05_claude_cli_interaction_.md), and how [Sandboxing](06_sandboxing_.md) keeps things secure. + +Now, let's look at how `claudia` handles the constant flow of information coming *from* the `claude` binary while it's running. This is the concept of **Streamed Output Processing**. + +## The Problem: Real-time Updates + +Imagine you ask Claude Code to perform a complex task, like analyzing your codebase or generating a long piece of documentation. This process can take time. The `claude` command-line tool doesn't just wait until it's completely finished and then dump all the results at once. Instead, it often sends its output piece by piece: a thought process here, a tool call there, a chunk of generated text, and finally, a result message. + +As a user of `claudia`'s graphical interface, you don't want to stare at a frozen screen waiting for everything to finish. You want to see what Claude is doing *right now*, as it's happening. You want a live view of its progress. + +This is the problem that Streamed Output Processing solves. `claudia` needs to capture this real-time, piece-by-piece output from the `claude` process and display it to you instantly. + +Think of it like watching a live news feed or a chat application. Messages appear as they are sent, not all bundled up and delivered at the very end. + +## What is Streamed Output Processing? + +Streamed Output Processing in `claudia` refers to the entire system that: + +1. **Captures** the output from the running `claude` process *as it is generated*. +2. **Receives** this output in the backend, often as a stream of data. +3. **Parses** this data (which is typically in a specific format called JSONL) line by line. +4. **Transforms** each parsed piece into a structured message that the frontend understands. +5. **Sends** these structured messages from the backend to the frontend immediately. +6. **Displays** these messages in the user interface as they arrive, providing a live, dynamic view. + +The core idea is that the output is treated as a *stream* – a continuous flow of data arriving over time – rather than a single large block of data at the end. + +## How it Looks in the UI + +When you execute an Agent or run an interactive session in `claudia`, the main part of the screen fills up with messages as they come in. + +You'll see different types of messages appear: + +* Initial system messages (showing session info, tools available). +* Assistant messages (Claude's thoughts, text, tool calls). +* User messages (your prompts, tool results sent back to Claude). +* Result messages (indicating the overall success or failure of a step). + +Each of these appears in the UI as soon as `claudia` receives the corresponding piece of output from the `claude` process. + +In the frontend code (like `src/components/AgentExecution.tsx` or `src/components/ClaudeCodeSession.tsx`), there's a state variable, typically an array, that holds all the messages displayed. When a new piece of output arrives, this array is updated, and React automatically re-renders the list to include the new message. + +For example, in `AgentExecution.tsx`, you'll find code like this managing the displayed messages: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... + +interface AgentExecutionProps { + // ... props ... +} + +export interface ClaudeStreamMessage { + type: "system" | "assistant" | "user" | "result"; + // ... other fields based on the JSONL structure ... +} + +export const AgentExecution: React.FC = ({ + agent, + onBack, + className, +}) => { + // State to hold the list of messages displayed in the UI + const [messages, setMessages] = useState([]); + // ... other state variables ... + + // ... handleExecute function ... + + // When a new message arrives (handled by an event listener, shown below): + const handleNewMessage = (newMessage: ClaudeStreamMessage) => { + setMessages(prev => [...prev, newMessage]); // Add the new message to the array + }; + + // ... render function ... + // The rendering logic maps over the `messages` array to display each one + // using the StreamMessage component + /* + return ( + // ... layout ... +
+ {messages.map((message, index) => ( + // Render each message + ))} +
+ // ... rest of component ... + ); + */ +}; +// ... rest of file ... +``` + +This state update (`setMessages`) is the frontend's way of saying, "Hey React, something new arrived, please update the list!" + +## How it Works: The Data Flow + +The communication happens in several steps, involving the `claude` binary, the operating system's pipes, the `claudia` backend (Rust), the Tauri framework, and the `claudia` frontend (TypeScript/React). + +1. **`claude` writes output:** The `claude` process executes your request. When it has a piece of output to share (like a tool call or a chunk of text), it writes it to its standard output (stdout). +2. **OS captures output:** Because `claudia`'s backend spawned `claude` with piped stdout ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)), the operating system redirects `claude`'s stdout into a temporary buffer or pipe that the `claudia` backend can read from. +3. **Backend reads line by line:** The `claudia` backend continuously reads from this pipe. It's specifically looking for newline characters to know when a complete line (a complete JSONL entry) has arrived. +4. **Backend emits event:** As soon as the backend reads a complete line, it takes the raw string data and emits it as a Tauri event. These events have a specific name (like `"agent-output"` or `"claude-output"`) that the frontend is listening for. +5. **Tauri delivers event:** The Tauri framework acts as the messenger, efficiently delivering the event and its data payload from the backend Rust process to the frontend JavaScript process. +6. **Frontend receives event:** The frontend has registered event listeners using Tauri's event API. When an event with the matching name arrives, the registered callback function is executed. +7. **Frontend processes and updates:** The callback function receives the raw output line. It parses the JSONL string into a JavaScript object and updates the component's state (`messages` array). +8. **UI re-renders:** React detects the state change and updates only the necessary parts of the UI to display the new message. + +Here is a simplified sequence diagram for this process: + +```mermaid +sequenceDiagram + participant ClaudeCLI as claude binary + participant OS as OS Pipe + participant Backend as Backend Commands (Rust) + participant Tauri as Tauri Core + participant Frontend as Frontend UI (TS/React) + + ClaudeCLI->>OS: Write line (JSONL) to stdout + OS-->>Backend: Data available in pipe + Backend->>Backend: Read line from pipe + Backend->>Tauri: Emit event "claude-output" with line data + Tauri->>Frontend: Deliver event + Frontend->>Frontend: Receive event in listener + Frontend->>Frontend: Parse JSONL line to message object + Frontend->>Frontend: Update state (add message to list) + Frontend->>Frontend: UI re-renders + Frontend->>User: Display new message in UI +``` + +This flow repeats every time `claude` outputs a new line, providing the smooth, real-time updates you see in the `claudia` interface. + +## Diving into the Code + +Let's look at the relevant code snippets from both the backend (Rust) and the frontend (TypeScript). + +### Backend: Reading and Emitting + +As seen in [Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md), the backend uses `tokio` to handle the asynchronous reading of the process's standard output. It spawns a task that reads line by line and emits events. + +Here's a simplified look at the part of `src-tauri/src/commands/claude.rs` (or similar module) that does this: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::io::{AsyncBufReadExt, BufReader}; +use tauri::{AppHandle, Manager}; +use tokio::process::Command; // Assuming command is already built + +/// Helper function to spawn Claude process and handle streaming +async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> { + // ... Configure stdout/stderr pipes ... + cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped()); + + let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?; + + let stdout = child.stdout.take().ok_or("Failed to get stdout")?; + let stdout_reader = BufReader::new(stdout); + + // Spawn a task to read stdout line by line and emit events + let app_handle_stdout = app.clone(); // Clone handle for the async task + tokio::spawn(async move { + let mut lines = stdout_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + // Log or process the raw line + log::debug!("Claude stdout line: {}", line); + // Emit the line as an event to the frontend + let _ = app_handle_stdout.emit("claude-output", &line); // <-- Emitting the event! + } + log::info!("Finished reading Claude stdout."); + }); + + // ... Similar task for stderr ... + // ... Task to wait for process exit and emit completion event ... + + Ok(()) +} + +// Example Tauri command calling the helper +/* +#[tauri::command] +pub async fn execute_claude_code(app: AppHandle, project_path: String, prompt: String, model: String) -> Result<(), String> { + // ... build the Command object 'cmd' ... + spawn_claude_process(app, cmd).await // Calls the streaming helper +} +*/ +``` + +The crucial part here is the `tokio::spawn` block that reads lines (`lines.next_line().await`) and, for each line, calls `app_handle_stdout.emit("claude-output", &line)`. This sends the raw JSONL line string to the frontend via the Tauri event system. The `"claude-output"` string is the event name. + +### Frontend: Listening and Processing + +In the frontend (TypeScript), the component that displays the output (like `AgentExecution.tsx` or `ClaudeCodeSession.tsx`) needs to set up listeners for these events when it loads and clean them up when it unmounts. + +Here's a simplified look at the event listener setup in `AgentExecution.tsx`: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +// ... ClaudeStreamMessage type ... + +export const AgentExecution: React.FC = ({ + agent, + onBack, + className, +}) => { + const [messages, setMessages] = useState([]); + const [rawJsonlOutput, setRawJsonlOutput] = useState([]); // Keep raw output too + // ... other state variables ... + + const unlistenRefs = useRef([]); // Ref to store unlisten functions + + useEffect(() => { + // Set up event listeners when the component mounts or execution starts + let outputUnlisten: UnlistenFn | undefined; + let errorUnlisten: UnlistenFn | undefined; + let completeUnlisten: UnlistenFn | undefined; + + const setupListeners = async () => { + try { + // Listen for lines from stdout + outputUnlisten = await listen("agent-output", (event) => { // <-- Listening for the event! + try { + // The event payload is the raw JSONL line string + const rawLine = event.payload; + setRawJsonlOutput(prev => [...prev, rawLine]); // Store raw line + + // Parse the JSONL string into a JavaScript object + const message = JSON.parse(rawLine) as ClaudeStreamMessage; + + // Update the messages state, triggering a UI re-render + setMessages(prev => [...prev, message]); // <-- Updating state! + + } catch (err) { + console.error("Failed to process Claude output line:", err, event.payload); + // Handle parsing errors if necessary + } + }); + + // Listen for stderr lines (errors) + errorUnlisten = await listen("agent-error", (event) => { + console.error("Claude stderr:", event.payload); + // You might want to display these errors in the UI too + }); + + // Listen for the process completion event + completeUnlisten = await listen("agent-complete", (event) => { + console.log("Claude process complete:", event.payload); + // Update UI state (e.g., hide loading indicator) + // ... update isRunning state ... + }); + + // Store unlisten functions so we can clean them up later + unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten]; + + } catch (err) { + console.error("Failed to set up event listeners:", err); + // Handle listener setup errors + } + }; + + setupListeners(); + + // Clean up listeners when the component unmounts + return () => { + unlistenRefs.current.forEach(unlisten => unlisten()); + }; + }, []); // Empty dependency array means setup runs once on mount + + // ... render function ... +}; +// ... rest of file ... +``` + +This `useEffect` hook sets up the listener using `listen("agent-output", ...)`. The callback function receives the event, accesses the raw JSONL string via `event.payload`, parses it with `JSON.parse`, and then updates the `messages` state using `setMessages`. This sequence is the core of the streamed output processing on the frontend. The `useRef` and the cleanup function in the `useEffect` are standard React patterns for managing resources (like event listeners) that persist across renders but need to be cleaned up when the component is no longer needed. + +The parsed `message` object is then passed down to the `StreamMessage` component (referenced in the provided code snippet for `src/components/StreamMessage.tsx`) which knows how to interpret the different `type` and `subtype` fields (like "assistant", "tool_use", "tool_result", "result") and render them with appropriate icons, formatting, and potentially syntax highlighting (using libraries like `react-markdown` and `react-syntax-highlighter`) or custom widgets ([ToolWidgets.tsx]). + +## Conclusion + +In this chapter, we explored **Streamed Output Processing**, understanding how `claudia` handles the real-time flow of information from the running `claude` command-line tool. We learned that `claude` sends output piece by piece in JSONL format, and that `claudia`'s backend captures this stream, reads it line by line, and immediately emits each line as a Tauri event to the frontend. + +On the frontend, we saw how components use `listen` to subscribe to these events, parse the JSONL payload into structured message objects, and update their state to display the new information dynamically. This entire process ensures that the `claudia` UI provides a responsive, live view of the AI's progress and actions during interactive sessions and Agent runs. + +Understanding streamed output is key to seeing how `claudia` provides its core real-time chat and execution experience on top of a command-line binary. + +In the next chapter, we'll look at how `claudia` keeps track of multiple potentially running processes, like Agent runs or direct sessions: [Process Registry](08_process_registry_.md). + +[Next Chapter: Process Registry](08_process_registry_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/StreamMessage.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ToolWidgets.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/types/enhanced-messages.ts) + +# Chapter 8: Process Registry + +Welcome back to the `claudia` tutorial! In our last chapter, [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md), we learned how `claudia` captures and displays the output from the `claude` command-line tool in real-time as it's running. + +Now, let's talk about something that happens just *before* that output starts streaming: launching the `claude` tool itself. When you click "Execute" for an Agent or start a new session, `claudia` doesn't just run the command and wait; it starts the `claude` binary as a separate **process** that runs in the background. + +What if you run multiple agents? What if you start a session and then switch to look at something else while it's running? How does `claudia` keep track of all these separate `claude` processes? How does it know which process is which? And how can it show you their status or let you stop them if needed? + +This is where the **Process Registry** comes in. + +## What is the Process Registry? + +Think of the Process Registry as `claudia`'s internal "Task Manager" specifically for the `claude` processes it starts. It's a system within the `claudia` backend (the Rust code) that keeps a list of all the `claude` processes that are currently running. + +For each running process, the registry stores important information, such as: + +* A unique identifier for this specific "run" (like the `run_id` we saw for Agent Runs in [Chapter 2: Agents](02_agents_.md)). +* The **Process ID (PID)** assigned by the operating system. This is like the process's unique phone number that the operating system uses to identify it. +* The current **status** (like "running", "completed", "failed", "cancelled"). +* Information about *what* is being run (like which Agent, the task description, the project path). +* A reference to the process itself, allowing `claudia` to interact with it (like sending a signal to stop it). +* A temporary buffer to hold the most recent output, allowing quick access to live status without reading the entire JSONL file every time. + +The Process Registry allows `claudia` to monitor these background processes, provide access to their live output streams (as discussed in [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)), and offer controls like stopping a running task. + +## The Use Case: Managing Running Sessions + +The most visible use case for the Process Registry in `claudia` is the "Running Sessions" screen. This screen lists all the Agent runs or interactive sessions that `claudia` has started and are still considered "active" (running or perhaps recently finished but not yet fully cleaned up). + +Here's a simplified look at the frontend component responsible for this, `RunningSessionsView.tsx`: + +```typescript +// src/components/RunningSessionsView.tsx (Simplified) +import { useState, useEffect } from 'react'; +// ... other imports ... +import { api } from '@/lib/api'; // Import API helper +import type { AgentRun } from '@/lib/api'; // Import data type + +export function RunningSessionsView({ /* ... props ... */ }) { + const [runningSessions, setRunningSessions] = useState([]); // State to hold list + const [loading, setLoading] = useState(true); + // ... other state ... + + // Function to fetch the list of running sessions + const loadRunningSessions = async () => { + try { + // Call the backend command to get running sessions + const sessions = await api.listRunningAgentSessions(); + setRunningSessions(sessions); // Update state with the list + } catch (error) { + console.error('Failed to load running sessions:', error); + // ... handle error ... + } finally { + setLoading(false); + } + }; + + // Function to stop a session + const killSession = async (runId: number, agentName: string) => { + try { + // Call the backend command to kill a session + const success = await api.killAgentSession(runId); + if (success) { + console.log(`${agentName} session stopped.`); + // Refresh the list after killing + await loadRunningSessions(); + } else { + console.warn('Session may have already finished'); + } + } catch (error) { + console.error('Failed to kill session:', error); + // ... handle error ... + } + }; + + useEffect(() => { + loadRunningSessions(); // Load sessions when component mounts + + // Set up auto-refresh + const interval = setInterval(() => { + loadRunningSessions(); + }, 5000); // Refresh every 5 seconds + + return () => clearInterval(interval); // Clean up interval + }, []); + + if (loading) { + return

Loading running sessions...

; // Loading indicator + } + + return ( +
+

Running Agent Sessions

+ {runningSessions.length === 0 ? ( +

No agent sessions are currently running

+ ) : ( +
+ {/* Map over the runningSessions list to display each one */} + {runningSessions.map((session) => ( +
{/* Card or similar display */} +

{session.agent_name}

+

Status: {session.status}

+

PID: {session.pid}

+ {/* ... other details like task, project path, duration ... */} + + {/* Buttons to interact with the session */} + {/* Set state to open viewer */} + +
+ ))} +
+ )} + + {/* Session Output Viewer component (shown when selectedSession is not null) */} + {selectedSession && ( + setSelectedSession(null)} + /> + )} +
+ ); +} +``` + +This component demonstrates how the frontend relies on the backend's Process Registry: +1. It calls `api.listRunningAgentSessions()` to get the current list. +2. It displays information for each running process, including the PID and status. +3. It provides "Stop" buttons that call `api.killAgentSession(runId)`, requesting the backend to terminate the corresponding process. +4. It provides a "View Output" button that, when clicked, might fetch the live output buffer from the registry (using a command like `api.getLiveSessionOutput(runId)`) before potentially switching to file-based streaming ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)). +5. It automatically refreshes this list periodically by calling `loadRunningSessions` again. + +## How it Works: Under the Hood + +The Process Registry is implemented in the Rust backend, primarily in the `src-tauri/src/process/registry.rs` file. + +Here's a simplified look at what happens step-by-step: + +1. **Process Spawned:** When a backend command like `execute_agent` or `execute_claude_code` needs to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)), it prepares the command and then calls `child.spawn()`. +2. **Registration:** Immediately after `child.spawn()` successfully starts the process, the backend extracts the **PID** from the returned `Child` object. It then takes the `run_id` (generated when the Agent run record was created in the database), the PID, and other relevant info (Agent name, task, project path) and calls a method on the `ProcessRegistry` instance, typically `registry.register_process(...)`. +3. **Registry Storage:** The `ProcessRegistry` stores this information in an in-memory data structure, like a `HashMap`, where the key is the `run_id` and the value is an object containing the `ProcessInfo` and the actual `Child` handle. It also initializes a buffer for live output for this specific run. +4. **Output Appending:** As the streaming output processing ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)) reads lines from the process's stdout/stderr pipes, it also appends these lines to the live output buffer associated with this run_id in the Process Registry using `registry.append_live_output(run_id, line)`. +5. **Listing Processes:** When the frontend calls `list_running_agent_sessions` (which maps to a backend command like `list_running_sessions`), the backend accesses the `ProcessRegistry` and asks it for the list of currently registered processes (`registry.get_running_processes()`). The registry returns the stored `ProcessInfo` for each active entry in its map. +6. **Viewing Live Output:** When the frontend calls `get_live_session_output(runId)`, the backend asks the registry for the live output buffer associated with that `runId` (`registry.get_live_output(runId)`), and returns it to the frontend. +7. **Killing Process:** When the frontend calls `kill_agent_session(runId)`, the backend first tells the `ProcessRegistry` to attempt to terminate the process (`registry.kill_process(runId)`). The registry uses the stored `Child` handle or PID to send a termination signal to the operating system. After attempting the kill, the backend also updates the database record for that run to mark its status as 'cancelled'. +8. **Cleanup:** Periodically, `claudia` runs a cleanup task (`cleanup_finished_processes`) that checks the status of processes currently in the registry. If a process has exited (e.g., finished naturally or was killed), the registry removes its entry (`registry.unregister_process(runId)`). This also helps keep the database status accurate. + +Here's a simple sequence diagram showing the core interactions: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI + participant Backend as Backend Commands + participant Registry as Process Registry + participant OS as Operating System + + User->>Frontend: Open Running Sessions View + Frontend->>Backend: Call list_running_sessions() + Backend->>Registry: get_running_processes() + Registry-->>Backend: Return List + Backend-->>Frontend: Return List (mapped from ProcessInfo) + Frontend->>User: Display List + + User->>Frontend: Click Stop Button (for runId) + Frontend->>Backend: Call kill_agent_session(runId) + Backend->>Registry: kill_process(runId) + Registry->>OS: Send terminate signal (using PID/Handle) + OS-->>Registry: Confirmation/Status + Registry-->>Backend: Return success/failure + Backend->>Backend: Update AgentRun status in DB + Backend-->>Frontend: Return confirmation + Frontend->>Frontend: Refresh list / Update UI +``` + +This diagram illustrates how the frontend relies on backend commands to query and manage the processes tracked by the Process Registry. + +## Diving into the Backend Code + +The core implementation of the Process Registry is found in `src-tauri/src/process/registry.rs`. + +First, let's look at the `ProcessInfo` struct, which holds the basic details about a running process: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... imports ... +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// Information about a running agent process +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcessInfo { + pub run_id: i64, // Matches the agent_runs database ID + pub agent_id: i64, // Which agent started this run + pub agent_name: String, // Agent's name + pub pid: u32, // Operating System Process ID + pub started_at: DateTime, // When it started + pub project_path: String, // Where it's running + pub task: String, // The task given + pub model: String, // The model used +} +``` + +The `ProcessRegistry` struct itself is simple; it just holds the map and uses `Arc>` for thread-safe access because multiple parts of the backend might need to interact with it concurrently. + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... imports ... +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use tokio::process::Child; // Need the process handle itself + +/// Information about a running process with handle +pub struct ProcessHandle { + pub info: ProcessInfo, + pub child: Arc>>, // The handle to the child process + pub live_output: Arc>, // Buffer for live output +} + +/// Registry for tracking active agent processes +pub struct ProcessRegistry { + // Map from run_id to the ProcessHandle + processes: Arc>>, +} + +impl ProcessRegistry { + pub fn new() -> Self { + Self { + processes: Arc::new(Mutex::new(HashMap::new())), + } + } + + // ... methods like register_process, unregister_process, get_running_processes, kill_process, append_live_output, get_live_output ... +} + +// Tauri State wrapper for the registry +pub struct ProcessRegistryState(pub Arc); +// ... Default impl ... +``` + +When a process is spawned, the `execute_agent` command (in `src-tauri/src/commands/agents.rs`) calls `registry.register_process`: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +// ... imports ... +// Assuming 'registry' is the State +// Assuming 'child' is the tokio::process::Child from cmd.spawn()... +// Assuming 'run_id', 'agent_id', etc., are defined... + +// Register the process in the registry +registry.0.register_process( + run_id, + agent_id, + agent.name.clone(), // Agent name + pid, // Process ID + project_path.clone(), + task.clone(), + execution_model.clone(), + child, // Pass the child handle +).map_err(|e| format!("Failed to register process: {}", e))?; + +info!("📋 Registered process in registry"); + +// ... rest of the async task waiting for process to finish ... +``` + +The `register_process` method in the `ProcessRegistry` then locks the internal map and inserts the new entry: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... + +/// Register a new running process +pub fn register_process( + &self, + run_id: i64, + agent_id: i64, + agent_name: String, + pid: u32, + project_path: String, + task: String, + model: String, + child: Child, // Receives the child handle +) -> Result<(), String> { + let mut processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + + let process_info = ProcessInfo { + run_id, agent_id, agent_name, pid, + started_at: Utc::now(), + project_path, task, model, + }; + + let process_handle = ProcessHandle { + info: process_info, + child: Arc::new(Mutex::new(Some(child))), // Store the handle + live_output: Arc::new(Mutex::new(String::new())), // Init output buffer + }; + + processes.insert(run_id, process_handle); // Insert into the map + Ok(()) +} +``` + +Listing running processes involves locking the map and collecting the `ProcessInfo` from each `ProcessHandle`: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... + +/// Get all running processes +pub fn get_running_processes(&self) -> Result, String> { + let processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + // Iterate through the map's values (ProcessHandle), clone the info field, collect into a Vec + Ok(processes.values().map(|handle| handle.info.clone()).collect()) +} +``` + +Killing a process involves looking up the `ProcessHandle` by `run_id`, accessing the stored `Child` handle, and calling its `kill` method: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... +use tokio::process::Child; + +/// Kill a running process +pub async fn kill_process(&self, run_id: i64) -> Result { + let processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + + if let Some(handle) = processes.get(&run_id) { + let child_arc = handle.child.clone(); + drop(processes); // IMPORTANT: Release the lock before calling async kill() + + let mut child_guard = child_arc.lock().map_err(|e| e.to_string())?; // Lock the child handle + if let Some(ref mut child) = child_guard.as_mut() { + match child.kill().await { // Call the async kill method + Ok(_) => { + *child_guard = None; // Clear the handle after killing + Ok(true) + } + Err(e) => Err(format!("Failed to kill process: {}", e)), + } + } else { + Ok(false) // Process was already killed or completed + } + } else { + Ok(false) // Process not found in registry + } +} +``` + +Note that the `kill_agent_session` Tauri command ([src-tauri/src/commands/agents.rs]) first calls `registry.kill_process` to try terminating the *actual* OS process via the `Child` handle, and *then* updates the database status. This ensures the UI accurately reflects the state even if the process doesn't immediately exit after the signal. + +The `cleanup_finished_processes` command (also in `src-tauri/src/commands/agents.rs`) periodically checks all processes currently in the registry using `registry.is_process_running()` and, if they are no longer running, updates their status in the database and removes them from the registry. + +This Process Registry provides the backend's central point for managing and interacting with all the separate `claude` instances that `claudia` is running, enabling features like the "Running Sessions" view and the ability to stop tasks. + +## Conclusion + +In this chapter, we introduced the **Process Registry**, `claudia`'s internal system for tracking the `claude` command-line tool processes it launches in the background. We learned that it stores essential information like PID, status, and associated run details, allowing `claudia` to monitor and control these separate tasks. + +We saw how the Process Registry is used to power features like the "Running Sessions" view in the UI, enabling users to see what's currently executing, view live output, and stop processes. We also delved into the backend implementation, seeing how processes are registered upon spawning, how the registry stores their handles, and how backend commands interact with the registry to list, kill, and manage these running tasks. + +Understanding the Process Registry is key to seeing how `claudia` manages concurrency and provides visibility and control over the AI tasks running on your system. + +In the next chapter, we'll explore **Checkpointing**, a feature that allows Claude Code to save and restore its state, enabling longer, more complex interactions across multiple runs. + +[Next Chapter: Checkpointing](09_checkpointing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/process/mod.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/process/registry.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/RunningSessionsView.tsx) + +# Chapter 9: Checkpointing + +Welcome back to the `claudia` tutorial! In our previous chapter, [Chapter 8: Process Registry](08_process_registry_.md), we learned how `claudia` keeps track of and manages the individual `claude` processes it launches. This allows the application to handle multiple running tasks simultaneously and provides a view of what's currently active. + +Now, let's talk about preserving the state of those tasks over time, even after they finish or the application closes. This is the powerful concept of **Checkpointing**. + +## The Problem: Sessions Are Temporary + +Imagine you're working with Claude Code on a complex feature development within a project. You have a long conversation, make several changes to files, get some code snippets, debug an issue, and maybe even use tools to run tests. This interaction might span hours or even days across multiple `claude` runs. + +Each run of `claude` is a session ([Chapter 1: Session/Project Management](01_session_project_management_.md)), and the CLI automatically saves the message history for that session. But what about the state of your project files? What if you want to go back to how the files looked *before* Claude made a specific set of changes? What if you want to experiment with a different approach, but keep the option to return to the current state? + +The basic session history saves the *conversation*, but it doesn't version control your *project files*. This is where checkpoints become essential. + +Think of it like writing a book. The message history is like your rough draft – a linear flow of words. But sometimes you want to save a specific version (e.g., "finished Chapter 5"), experiment with rewriting a scene, and maybe decide later to revert to that saved version or start a new version branched from it. Checkpointing provides this capability for your AI-assisted coding sessions. + +## What is Checkpointing? + +Checkpointing in `claudia` is a system for creating save points of your entire working state for a specific Claude Code session. A checkpoint captures two main things at a particular moment: + +1. **The complete message history** up to that point in the session. +2. **Snapshots of your project files** that have changed since the last checkpoint (or are being tracked). + +When you create a checkpoint, `claudia` records the session's conversation history and saves copies of the relevant files in a special location. This lets you revisit that exact moment later. + +**In simpler terms:** + +* A Checkpoint is a snapshot of your conversation *and* your project files at a specific point in time. +* You can create checkpoints manually whenever you want to save a significant state (like "After implementing Login feature"). +* `claudia` can also create checkpoints automatically based on certain events (like after a tool makes changes to files). +* Checkpoints are organized in a **Timeline**, showing the history of your session like a branching tree (similar to how git commits work). +* You can **Restore** a checkpoint to revert your message history and project files to that saved state. +* You can **Fork** from a checkpoint to start a new conversation branch from a previous state. +* You can **Diff** between checkpoints to see exactly which files were changed and what the changes were. + +## Key Concepts in Checkpointing + +Let's break down the core ideas behind `claudia`'s checkpointing system: + +| Concept | Description | Analogy | +| :----------------- | :--------------------------------------------------------------------------------------------------------- | :---------------------------------------------- | +| **Checkpoint** | A specific save point containing session messages and file snapshots. | Saving your game progress. | +| **Timeline** | The chronological history of checkpoints for a session, shown as a tree structure reflecting branching (forks). | A Git history tree or a family tree. | +| **File Snapshot** | A saved copy of a project file's content and metadata at a specific checkpoint. Only saves changes efficiently. | Saving individual changed files in a commit. | +| **Restoring** | Reverting the current session messages and project files to the state captured in a chosen checkpoint. | Loading a previous save game. | +| **Forking** | Creating a new session branch starting from a specific checkpoint. | Branching in Git or creating an alternate story. | +| **Automatic Checkpoints** | Checkpoints created by `claudia` based on predefined rules (e.g., after certain actions). | Auto-save feature in software. | +| **Checkpoint Strategy** | The specific rule defining when automatic checkpoints are created (Per Prompt, Per Tool Use, Smart). | Different auto-save frequencies/triggers. | +| **Diffing** | Comparing two checkpoints to see the differences in file content and token usage. | `git diff` command. | + +## Using Checkpointing in the UI + +You interact with checkpointing primarily within a specific session view (like `ClaudeCodeSession.tsx`), typically via a dedicated section or side panel. + +The `TimelineNavigator.tsx` component is the central piece of the UI for browsing and interacting with checkpoints: + +```typescript +// src/components/TimelineNavigator.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { GitBranch, Save, RotateCcw, GitFork, Diff } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { api, type Checkpoint, type TimelineNode, type SessionTimeline, type CheckpointDiff } from "@/lib/api"; // Import types and API + +// ... component props interface ... + +/** + * Visual timeline navigator for checkpoint management + */ +export const TimelineNavigator: React.FC = ({ + sessionId, + projectId, + projectPath, + currentMessageIndex, + onCheckpointSelect, // Callback for selecting a checkpoint (e.g., for Diff) + onFork, // Callback for triggering a fork + refreshVersion = 0, // Prop to force reload + className +}) => { + const [timeline, setTimeline] = useState(null); // State for the timeline data + const [selectedCheckpoint, setSelectedCheckpoint] = useState(null); // State for the currently selected checkpoint (for diffing, etc.) + const [showCreateDialog, setShowCreateDialog] = useState(false); // State for the "Create Checkpoint" dialog + const [checkpointDescription, setCheckpointDescription] = useState(""); // State for the description input + const [isLoading, setIsLoading] = useState(false); + // ... other state for diff dialog, errors, etc. ... + + // Effect to load the timeline when the component mounts or needs refreshing + useEffect(() => { + loadTimeline(); + }, [sessionId, projectId, projectPath, refreshVersion]); // Dependencies + + // Function to load timeline data from backend + const loadTimeline = async () => { + try { + setIsLoading(true); + // Call backend API to get the timeline + const timelineData = await api.getSessionTimeline(sessionId, projectId, projectPath); + setTimeline(timelineData); // Update state + // ... logic to auto-expand current branch ... + } catch (err) { + console.error("Failed to load timeline:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle manual checkpoint creation + const handleCreateCheckpoint = async () => { + try { + setIsLoading(true); + // Call backend API to create a checkpoint + await api.createCheckpoint( + sessionId, + projectId, + projectPath, + currentMessageIndex, // Pass current message count + checkpointDescription || undefined // Pass optional description + ); + setCheckpointDescription(""); // Clear input + setShowCreateDialog(false); // Close dialog + await loadTimeline(); // Reload timeline to show the new checkpoint + } catch (err) { + console.error("Failed to create checkpoint:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle restoring a checkpoint + const handleRestoreCheckpoint = async (checkpoint: Checkpoint) => { + // ... confirmation logic ... + try { + setIsLoading(true); + // Call backend API to restore the checkpoint + await api.restoreCheckpoint(checkpoint.id, sessionId, projectId, projectPath); + await loadTimeline(); // Reload timeline + // Notify parent component or session view about the restore + // This might trigger reloading the message history from the checkpoint + onCheckpointSelect(checkpoint); + } catch (err) { + console.error("Failed to restore checkpoint:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle forking (delegates to parent component via callback) + const handleFork = async (checkpoint: Checkpoint) => { + // This component doesn't *create* the new session, it tells the parent + // session view to initiate a fork from this checkpoint ID + onFork(checkpoint.id); + }; + + // Function to handle comparing checkpoints + const handleCompare = async (checkpoint: Checkpoint) => { + if (!selectedCheckpoint) { + // If no checkpoint is selected for comparison, select this one + setSelectedCheckpoint(checkpoint); + // You might update UI to show this checkpoint is selected for compare + return; + } + // If a checkpoint is already selected, perform the comparison + try { + setIsLoading(true); + const diffData = await api.getCheckpointDiff( + selectedCheckpoint.id, // The first selected checkpoint + checkpoint.id, // The checkpoint being compared against + sessionId, projectId // Session/Project context + ); + // ... show diffData in a dialog ... + setDiff(diffData); + // ... open diff dialog ... + } catch (err) { + console.error("Failed to get diff:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + + // Recursive function to render the timeline tree structure + const renderTimelineNode = (node: TimelineNode, depth: number = 0) => { + // ... rendering logic for node, its children, and buttons ... + // Each node displays checkpoint info and buttons for Restore, Fork, Diff + const isCurrent = timeline?.currentCheckpointId === node.checkpoint.id; + const isSelected = selectedCheckpoint?.id === node.checkpoint.id; // For compare selection + + + return ( +
+ {/* UI representation of the checkpoint */} + setSelectedCheckpoint(node.checkpoint)} // Select for compare/info + > + + {/* Display checkpoint ID, timestamp, description, metadata (tokens, files) */} +

{node.checkpoint.id.slice(0, 8)}...

+

{node.checkpoint.timestamp}

+

{node.checkpoint.description}

+ {node.checkpoint.metadata.totalTokens} tokens + {node.checkpoint.metadata.fileChanges} files changed + + {/* Action Buttons */} + + + +
+
+ + {/* Recursively render children */} + {/* ... Conditional rendering based on expanded state ... */} +
+ {node.children.map((child) => renderTimelineNode(child, depth + 1))} +
+
+ ); + }; + + return ( +
+ {/* ... Warning message ... */} + {/* Header with "Checkpoint" button */} +
+
+ +

Timeline

+ {/* Display total checkpoints badge */} +
+ +
+ + {/* Error display */} + {/* ... */} + + {/* Render the timeline tree starting from the root node */} + {timeline?.rootNode ? ( +
+ {renderTimelineNode(timeline.rootNode)} +
+ ) : ( + // ... Loading/empty state ... + )} + + {/* Create checkpoint dialog */} + + + + Create Checkpoint + {/* ... Dialog description and input for description ... */} + +
+
+ + setCheckpointDescription(e.target.value)} /> +
+
+ + {/* ... Cancel and Create buttons calling handleCreateCheckpoint ... */} + +
+
+ + {/* Diff dialog (not shown here, but would display diff state) */} + {/* ... Dialog for showing diff results ... */} +
+ ); +}; +``` + +This component displays the timeline tree structure, fetched from the backend using `api.getSessionTimeline`. Each node in the tree represents a checkpoint (`TimelineNode` contains a `Checkpoint` struct). The component provides buttons to trigger actions like creating a manual checkpoint (`handleCreateCheckpoint`), restoring a checkpoint (`handleRestoreCheckpoint`), forking (`handleFork`), and comparing checkpoints (`handleCompare`). These actions call corresponding backend API functions via `src/lib/api.ts`. + +You can also configure automatic checkpointing and cleanup using the `CheckpointSettings.tsx` component: + +```typescript +// src/components/CheckpointSettings.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { Settings, Save, Trash2, HardDrive } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { SelectComponent } from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { api, type CheckpointStrategy } from "@/lib/api"; // Import types and API + +// ... component props interface ... + +/** + * CheckpointSettings component for managing checkpoint configuration + */ +export const CheckpointSettings: React.FC = ({ + sessionId, + projectId, + projectPath, + onClose, + className, +}) => { + const [autoCheckpointEnabled, setAutoCheckpointEnabled] = useState(true); + const [checkpointStrategy, setCheckpointStrategy] = useState("smart"); + const [totalCheckpoints, setTotalCheckpoints] = useState(0); + const [keepCount, setKeepCount] = useState(10); // State for cleanup setting + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + // ... error/success states ... + + const strategyOptions: SelectOption[] = [ + { value: "manual", label: "Manual Only" }, + { value: "per_prompt", label: "After Each Prompt" }, + { value: "per_tool_use", label: "After Tool Use" }, + { value: "smart", label: "Smart (Recommended)" }, + ]; + + // Load settings when component mounts + useEffect(() => { + loadSettings(); + }, [sessionId, projectId, projectPath]); + + const loadSettings = async () => { + try { + setIsLoading(true); + // Call backend API to get settings + const settings = await api.getCheckpointSettings(sessionId, projectId, projectPath); + setAutoCheckpointEnabled(settings.auto_checkpoint_enabled); + setCheckpointStrategy(settings.checkpoint_strategy); + setTotalCheckpoints(settings.total_checkpoints); // Get total count for cleanup info + } catch (err) { + console.error("Failed to load checkpoint settings:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + const handleSaveSettings = async () => { + try { + setIsSaving(true); + // Call backend API to update settings + await api.updateCheckpointSettings( + sessionId, + projectId, + projectPath, + autoCheckpointEnabled, + checkpointStrategy + ); + // ... show success message ... + } catch (err) { + console.error("Failed to save checkpoint settings:", err); + // ... set error state ... + } finally { + setIsSaving(false); + } + }; + + const handleCleanup = async () => { + // ... confirmation ... + try { + setIsLoading(true); + // Call backend API to cleanup + const removed = await api.cleanupOldCheckpoints( + sessionId, + projectId, + projectPath, + keepCount // Pass how many recent checkpoints to keep + ); + // ... show success message ... + await loadSettings(); // Refresh count + } catch (err) { + console.error("Failed to cleanup checkpoints:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* ... Experimental Warning ... */} + {/* Header */} +
+ {/* ... Title and icon ... */} + {onClose && } +
+ + {/* Error/Success messages */} + {/* ... */} + +
+ {/* Auto-checkpoint toggle */} +
+
+ +

Automatically create checkpoints

+
+ +
+ + {/* Checkpoint strategy select */} +
+ + setCheckpointStrategy(value as CheckpointStrategy)} + options={strategyOptions} + disabled={isLoading || !autoCheckpointEnabled} // Disable if auto-checkpoint is off + /> + {/* ... Strategy description text ... */} +
+ + {/* Save button */} + +
+ + {/* Storage Management Section */} +
+
+ {/* ... "Storage Management" title and icon ... */} +

Total checkpoints: {totalCheckpoints}

{/* Display count */} +
+ {/* Cleanup settings */} +
+ +
+ setKeepCount(parseInt(e.target.value) || 10)} disabled={isLoading} className="flex-1"/> + +
+ {/* ... Cleanup description text ... */} +
+
+ + ); +}; +``` + +This component allows you to toggle automatic checkpoints, select a strategy (Manual, Per Prompt, Per Tool Use, Smart), set how many recent checkpoints to keep, and trigger a cleanup. These actions are handled by backend commands called via `api`. + +## How it Works: Under the Hood (Backend) + +The checkpointing logic resides in the `src-tauri/src/checkpoint/` module. This module contains several key parts: + +1. **`checkpoint::mod.rs`**: Defines the main data structures (`Checkpoint`, `FileSnapshot`, `SessionTimeline`, `TimelineNode`, `CheckpointStrategy`, etc.) and utility structs (`CheckpointPaths`, `CheckpointDiff`). +2. **`checkpoint::storage.rs`**: Handles reading from and writing to disk. It manages saving/loading checkpoint metadata, messages, and file snapshots. It uses content-addressable storage for file contents to save space. +3. **`checkpoint::manager.rs`**: The core logic for managing a *single session*'s checkpoints. It tracks file changes (`FileTracker`), keeps the current message history (`current_messages`), interacts with `CheckpointStorage` for saving/loading, manages the session's `Timeline`, and handles operations like creating, restoring, and forking. +4. **`checkpoint::state.rs`**: A stateful manager (similar to the Process Registry) that holds `CheckpointManager` instances for *all active sessions* in memory. This prevents needing to recreate managers for each command call. + +Checkpoint data is stored within the `~/.claude` directory, specifically within the project's timeline directory: + +`~/.claude/projects//.timelines//` + +Inside this session timeline directory, you'll find: +* `timeline.json`: Stores the `SessionTimeline` structure (the tree metadata). +* `checkpoints/`: A directory containing subdirectories for each checkpoint ID. Each checkpoint directory (`checkpoints//`) holds `metadata.json` and `messages.jsonl` (the compressed messages). +* `files/`: A directory containing file snapshots, organized into a `content_pool/` (actual compressed file contents, stored by hash) and `refs/` (references from each checkpoint back to the content pool, stored as small JSON files). + +### The `CheckpointState` + +Just like the Process Registry manages active processes, the `CheckpointState` manages active `CheckpointManager` instances. When a session starts or is loaded in the UI, the frontend calls a backend command which then uses `CheckpointState::get_or_create_manager` to get the manager for that session. + +```rust +// src-tauri/src/checkpoint/state.rs (Simplified) +// ... imports ... +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; // For thread-safe async access + +use super::manager::CheckpointManager; + +/// Manages checkpoint managers for active sessions +#[derive(Default, Clone)] +pub struct CheckpointState { + /// Map of session_id to CheckpointManager + managers: Arc>>>, // Use RwLock for concurrent reads/writes + claude_dir: Arc>>, // Claude dir path needed for storage initialization +} + +impl CheckpointState { + // ... new(), set_claude_dir(), remove_manager(), clear_all() methods ... + + /// Gets or creates a CheckpointManager for a session + pub async fn get_or_create_manager( + &self, + session_id: String, + project_id: String, + project_path: PathBuf, + ) -> Result> { + let mut managers = self.managers.write().await; // Lock for writing + + // Check if manager already exists + if let Some(manager) = managers.get(&session_id) { + return Ok(Arc::clone(manager)); // Return existing manager (Arc::clone increases ref count) + } + + // ... get claude_dir ... + + // Create new manager if it doesn't exist + let manager = CheckpointManager::new( + project_id, + session_id.clone(), + project_path, + claude_dir, + ).await?; // CheckpointManager::new handles loading/init storage + + let manager_arc = Arc::new(manager); + managers.insert(session_id, Arc::clone(&manager_arc)); // Store new manager + + Ok(manager_arc) + } + + // ... get_manager(), list_active_sessions() methods ... +} +``` + +This structure ensures that the heavy work of loading the timeline and setting up file tracking only happens once per session when it's first accessed, not for every single checkpoint-related command. + +### Creating a Checkpoint Flow + +When the frontend requests to create a checkpoint (manually or automatically), the backend command retrieves the session's `CheckpointManager` from the `CheckpointState` and calls `manager.create_checkpoint(...)`. + +Here's a simplified look at what happens inside `CheckpointManager::create_checkpoint`: + +```rust +// src-tauri/src/checkpoint/manager.rs (Simplified) +// ... imports ... + +impl CheckpointManager { + // ... new(), track_message(), track_file_modification(), etc. ... + + /// Create a checkpoint + pub async fn create_checkpoint( + &self, + description: Option, + parent_checkpoint_id: Option, // Optional parent ID for explicit forks + ) -> Result { + let messages = self.current_messages.read().await; // Get current messages + let message_index = messages.len().saturating_sub(1); + + // ... Extract metadata (prompt, tokens, etc.) from messages ... + + // Ensure all files in the project are tracked before snapshotting + // This discovers new files and adds them to the file tracker + let mut all_files = Vec::new(); + let _ = collect_files(&self.project_path, &self.project_path, &mut all_files); + for rel in all_files { + if let Some(p) = rel.to_str() { + let _ = self.track_file_modification(p).await; // Adds/updates tracker state + } + } + + // Generate a unique ID for the new checkpoint + let checkpoint_id = storage::CheckpointStorage::generate_checkpoint_id(); + + // Create file snapshots based on the *current* state of tracked files + // This reads the content of files marked as modified by track_file_modification + let file_snapshots = self.create_file_snapshots(&checkpoint_id).await?; + + // Build the Checkpoint metadata struct + let checkpoint = Checkpoint { + id: checkpoint_id.clone(), + session_id: self.session_id.clone(), + project_id: self.project_id.clone(), + message_index, + timestamp: Utc::now(), + description, + parent_checkpoint_id: parent_checkpoint_id.or_else(|| self.timeline.read().await.current_checkpoint_id.clone()), // Link to current parent or explicit parent + // ... include extracted metadata ... + }; + + // Save the checkpoint using the storage layer + let messages_content = messages.join("\n"); + let result = self.storage.save_checkpoint( + &self.project_id, + &self.session_id, + &checkpoint, + file_snapshots, // Pass the actual snapshots + &messages_content, // Pass the message content + )?; + + // ... Reload timeline from disk to incorporate new node ... + // ... Update current_checkpoint_id in in-memory timeline ... + // ... Reset is_modified flag in the file tracker ... + + Ok(result) + } + + // Helper to create FileSnapshots from the FileTracker state + async fn create_file_snapshots(&self, checkpoint_id: &str) -> Result> { + let tracker = self.file_tracker.read().await; + let mut snapshots = Vec::new(); + + for (rel_path, state) in &tracker.tracked_files { + // Only snapshot files marked as modified or deleted + if !state.is_modified && state.exists { // Only include if modified OR was deleted + continue; // Skip if not modified AND still exists + } + if state.is_modified || !state.exists { // Snapshot if modified or is now deleted + // ... read file content, calculate hash, get metadata ... + let (content, exists, permissions, size, current_hash) = { /* ... */ }; + + snapshots.push(FileSnapshot { + checkpoint_id: checkpoint_id.to_string(), + file_path: rel_path.clone(), + content, // Content will be empty for deleted files + hash: current_hash, // Hash will be empty for deleted files + is_deleted: !exists, + permissions, + size, + }); + } + } + Ok(snapshots) + } + + // ... other methods ... +} +``` + +The `create_checkpoint` function coordinates the process: it reads current messages, identifies changed files using the `FileTracker`, generates file snapshots by reading changed file contents, creates the checkpoint metadata, saves everything to disk via `CheckpointStorage`, and updates the timeline. + +The `FileTracker` keeps a list of files that have been referenced (either by the user or by tool outputs). The `track_file_modification` method is called whenever a file might have changed (e.g., mentioned in an edit tool output). It checks the file's current state (existence, hash, modification time) and marks it as `is_modified` if it differs from the last known state. + +The `CheckpointStorage::save_checkpoint` method handles the actual disk writing, including compressing messages and file contents and managing the content-addressable storage for file snapshots (`save_file_snapshot`). + +```rust +// src-tauri/src/checkpoint/storage.rs (Simplified) +// ... imports ... + +impl CheckpointStorage { + // ... new(), init_storage(), load_checkpoint(), etc. ... + + /// Save a checkpoint to disk + pub fn save_checkpoint(/* ... arguments ... */) -> Result { + // ... create directories ... + // ... save metadata.json ... + // ... save compressed messages.jsonl ... + + // Save file snapshots (calling save_file_snapshot for each) + let mut files_processed = 0; + for snapshot in &file_snapshots { + if self.save_file_snapshot(&paths, snapshot).is_ok() { // Calls helper + files_processed += 1; + } + } + + // Update timeline file on disk + self.update_timeline_with_checkpoint(/* ... */)?; + + // ... return result ... + Ok(CheckpointResult { /* ... */ }) + } + + /// Save a single file snapshot using content-addressable storage + fn save_file_snapshot(&self, paths: &CheckpointPaths, snapshot: &FileSnapshot) -> Result<()> { + // Directory where actual file content is stored by hash + let content_pool_dir = paths.files_dir.join("content_pool"); + fs::create_dir_all(&content_pool_dir)?; + + // Path to the content file based on its hash + let content_file = content_pool_dir.join(&snapshot.hash); + + // Only write content if the file doesn't exist (avoids duplicates) + if !content_file.exists() && !snapshot.is_deleted { + // Compress and save file content + let compressed_content = encode_all(snapshot.content.as_bytes(), self.compression_level) + .context("Failed to compress file content")?; + fs::write(&content_file, compressed_content)?; + } + + // Create a reference file for this checkpoint's view of the file + let checkpoint_refs_dir = paths.files_dir.join("refs").join(&snapshot.checkpoint_id); + fs::create_dir_all(&checkpoint_refs_dir)?; + + // Save a small JSON file containing metadata and a pointer (hash) to the content pool + let ref_metadata = serde_json::json!({ + "path": snapshot.file_path, + "hash": snapshot.hash, + "is_deleted": snapshot.is_deleted, + "permissions": snapshot.permissions, + "size": snapshot.size, + }); + let safe_filename = snapshot.file_path.to_string_lossy().replace('/', "_").replace('\\', "_"); + let ref_path = checkpoint_refs_dir.join(format!("{}.json", safe_filename)); + fs::write(&ref_path, serde_json::to_string_pretty(&ref_metadata)?)?; + + Ok(()) + } + + // ... update_timeline_with_checkpoint() and other methods ... +} +``` + +This snippet shows how `save_file_snapshot` stores the *actual* file content in a `content_pool` directory, named by the file's hash. This means if the same file content appears in multiple checkpoints, it's only stored once on disk. Then, in a `refs` directory specific to the checkpoint, a small file is saved that just contains the file's metadata and a pointer (the hash) back to the content pool. + +Here is a simplified sequence diagram for creating a manual checkpoint: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI (TimelineNavigator.tsx) + participant Backend as Backend Commands (claude.rs) + participant CheckpointState as CheckpointState (state.rs) + participant CheckpointManager as CheckpointManager (manager.rs) + participant CheckpointStorage as CheckpointStorage (storage.rs) + participant Filesystem as Filesystem + + User->>Frontend: Clicks "Checkpoint" button + Frontend->>Backend: Call create_checkpoint(...) + Backend->>CheckpointState: get_or_create_manager(session_id, ...) + CheckpointState->>CheckpointState: Look up manager in map + alt Manager exists + CheckpointState-->>Backend: Return existing manager + else Manager does not exist + CheckpointState->>CheckpointManager: Create new Manager() + CheckpointManager->>CheckpointStorage: init_storage(...) + CheckpointStorage->>Filesystem: Create directories, load timeline.json + Filesystem-->>CheckpointStorage: Return timeline data / Success + CheckpointStorage-->>CheckpointManager: Success + CheckpointManager-->>CheckpointState: Return new manager + CheckpointState->>CheckpointState: Store new manager in map + CheckpointState-->>Backend: Return new manager + end + Backend->>CheckpointManager: create_checkpoint(description, ...) + CheckpointManager->>CheckpointManager: Read current messages + CheckpointManager->>Filesystem: Walk project directory + Filesystem-->>CheckpointManager: List of files + loop For each project file + CheckpointManager->>Filesystem: Read file content & metadata + Filesystem-->>CheckpointManager: File data + CheckpointManager->>CheckpointManager: Track file state (hash, modified) + end + CheckpointManager->>CheckpointStorage: save_checkpoint(checkpoint, snapshots, messages) + CheckpointStorage->>Filesystem: Write metadata.json, messages.jsonl (compressed) + loop For each modified file + CheckpointStorage->>Filesystem: Check if hash exists in content_pool + alt Hash exists + CheckpointStorage->>Filesystem: Skip writing content + else Hash does not exist + CheckpointStorage->>Filesystem: Write compressed file content to content_pool (by hash) + end + CheckpointStorage->>Filesystem: Write reference file (metadata + hash) to refs/ + end + CheckpointStorage->>Filesystem: Update timeline.json + Filesystem-->>CheckpointStorage: Success + CheckpointStorage-->>CheckpointManager: Return success/result + CheckpointManager-->>Backend: Return success/result + Backend-->>Frontend: Resolve Promise + Frontend->>Frontend: Call loadTimeline() to refresh UI + Frontend->>User: Display new checkpoint in timeline +``` + +This diagram illustrates the flow from the user clicking a button to the backend coordinating with the manager, which in turn uses the storage layer to read and write data to the filesystem, resulting in a new checkpoint entry and updated timeline on disk. + +### Restoring a Checkpoint Flow + +Restoring a checkpoint works in reverse. When the frontend calls `api.restoreCheckpoint(checkpointId, ...)`, the backend finds the `CheckpointManager` and calls `manager.restore_checkpoint(checkpointId)`. + +```rust +// src-tauri/src/checkpoint/manager.rs (Simplified) +// ... imports ... + +impl CheckpointManager { + // ... create_checkpoint() etc. ... + + /// Restore a checkpoint + pub async fn restore_checkpoint(&self, checkpoint_id: &str) -> Result { + // Load checkpoint data using the storage layer + let (checkpoint, file_snapshots, messages) = self.storage.load_checkpoint( + &self.project_id, + &self.session_id, + checkpoint_id, + )?; + + // Get list of all files currently in the project directory + let mut current_files = Vec::new(); + let _ = collect_all_project_files(&self.project_path, &self.project_path, &mut current_files); + + // Determine which files need to be deleted (exist now, but not in snapshot as non-deleted) + let mut checkpoint_files_set = std::collections::HashSet::new(); + for snapshot in &file_snapshots { + if !snapshot.is_deleted { + checkpoint_files_set.insert(snapshot.file_path.clone()); + } + } + + // Delete files not present (as non-deleted) in the checkpoint + for current_file in current_files { + if !checkpoint_files_set.contains(¤t_file) { + let full_path = self.project_path.join(¤t_file); + // ... attempt fs::remove_file(&full_path) ... + log::info!("Deleted file not in checkpoint: {:?}", current_file); + } + } + // ... attempt to remove empty directories ... + + + // Restore/overwrite files from snapshots + let mut files_processed = 0; + for snapshot in &file_snapshots { + // This helper handles creating parent dirs, writing content, setting permissions, or deleting + match self.restore_file_snapshot(snapshot).await { // Calls helper + Ok(_) => { /* ... */ }, + Err(e) => { /* ... collect warnings ... */ }, + } + files_processed += 1; + } + + // Update in-memory messages buffer + let mut current_messages = self.current_messages.write().await; + current_messages.clear(); + for line in messages.lines() { + current_messages.push(line.to_string()); + } + + // Update the current_checkpoint_id in the in-memory timeline + let mut timeline = self.timeline.write().await; + timeline.current_checkpoint_id = Some(checkpoint_id.to_string()); + + // Reset the file tracker state to match the restored checkpoint + let mut tracker = self.file_tracker.write().await; + tracker.tracked_files.clear(); // Clear old state + for snapshot in &file_snapshots { + if !snapshot.is_deleted { + tracker.tracked_files.insert( + snapshot.file_path.clone(), + FileState { + last_hash: snapshot.hash.clone(), + is_modified: false, // Assume clean state after restore + last_modified: Utc::now(), // Or snapshot timestamp if available? + exists: true, + } + ); + } + } + + + Ok(CheckpointResult { /* ... checkpoint, files_processed, warnings ... */ }) + } + + // Helper to restore a single file from its snapshot data + async fn restore_file_snapshot(&self, snapshot: &FileSnapshot) -> Result<()> { + let full_path = self.project_path.join(&snapshot.file_path); + + if snapshot.is_deleted { + // If snapshot indicates deleted, remove the file if it exists + if full_path.exists() { + fs::remove_file(&full_path).context("Failed to delete file")?; + } + } else { + // If snapshot exists, create parent directories and write content + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).context("Failed to create parent directories")?; + } + fs::write(&full_path, &snapshot.content).context("Failed to write file")?; + + // Restore permissions (Unix only) + #[cfg(unix)] + if let Some(mode) = snapshot.permissions { + use std::os::unix::fs::PermissionsExt; + let permissions = std::fs::Permissions::from_mode(mode); + fs::set_permissions(&full_path, permissions).context("Failed to set file permissions")?; + } + } + Ok(()) + } + + // ... other methods ... +} +``` + +The `restore_checkpoint` function reads the checkpoint data from disk using `CheckpointStorage::load_checkpoint`. It then gets a list of the *current* files in the project directory. By comparing the current files with the files present in the checkpoint snapshot, it identifies which files need to be deleted. It iterates through the snapshots, using `restore_file_snapshot` to either delete files or write their content back to the project directory, recreating parent directories and setting permissions as needed. Finally, it updates the in-memory message list and the current checkpoint pointer in the timeline manager. + +This process effectively reverts the project directory and the session's state to match the chosen checkpoint. + +### Forking + +Forking is implemented by first restoring the session to the chosen checkpoint and then immediately creating a *new* checkpoint from that restored state. The key is that the new checkpoint explicitly sets its `parent_checkpoint_id` to the checkpoint it forked *from*, causing the timeline to branch. + +### Automatic Checkpointing + +Automatic checkpointing is controlled by the `auto_checkpoint_enabled` flag and the `checkpoint_strategy` setting stored in the `SessionTimeline`. When a new message arrives in the session (handled by the streaming output processing, [Chapter 7]), the `CheckpointManager::should_auto_checkpoint` method is called. This checks the strategy. For example, if the strategy is `PerPrompt`, it checks if the message is a user prompt. If the strategy is `Smart`, it checks if the message indicates a potentially destructive tool use (like `write`, `edit`, `bash`). If `should_auto_checkpoint` returns `true`, the backend triggers the `create_checkpoint` flow described above. + +### Cleanup + +The `Cleanup` feature in the `CheckpointSettings.tsx` component calls a backend command that uses `CheckpointStorage::cleanup_old_checkpoints`. This function loads the timeline, sorts checkpoints chronologically, identifies checkpoints older than the `keep_count`, and removes their metadata and references from disk. Crucially, it then calls `CheckpointStorage::garbage_collect_content` to find any actual file content in the `content_pool` directory that is *no longer referenced by any remaining checkpoints* and deletes that orphaned content to free up disk space. + +## Conclusion + +In this chapter, we delved into **Checkpointing**, a powerful feature in `claudia` that provides version control for your Claude Code sessions. We learned that checkpoints save snapshots of both your session's message history and the state of your project files, organized into a visual timeline. + +We explored how you can use the UI to create manual checkpoints, restore to previous states, fork off new branches of work, view differences between checkpoints, and configure automatic checkpointing and cleanup settings. + +Under the hood, we saw how the backend uses a `CheckpointManager` per session, coordinates with `CheckpointStorage` for reading and writing to disk, tracks file changes using a `FileTracker`, and uses a content-addressable storage mechanism for file snapshots to save disk space. We walked through the steps involved in creating and restoring checkpoints, including managing file changes and updating the session state. + +Understanding checkpointing empowers you to use Claude Code for more complex and iterative tasks with confidence, knowing you can always revert to a previous state or explore different paths. + +In the next and final chapter, we will explore **MCP (Model Context Protocol)**, the standardized format Claude Code uses for exchanging information with tools and other components, which plays a role in enabling features like checkpointing and tool execution. + +[Next Chapter: MCP (Model Context Protocol)](10_mcp__model_context_protocol__.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/manager.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/mod.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/state.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/storage.rs), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CheckpointSettings.tsx), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/TimelineNavigator.tsx) +# Chapter 10: MCP (Model Context Protocol) + +Welcome to the final chapter of the `claudia` tutorial! We've covered a lot, from managing your work with [Session/Project Management](01_session_project_management_.md) and defining specialized [Agents](02_agents_.md), to understanding how the [Frontend UI Components](03_frontend_ui_components_.md) are built and how they talk to the backend using [Tauri Commands](04_tauri_commands_.md). We've seen how `claudia` interacts with the core [Claude CLI Interaction](05_claude_cli_interaction_.md), how [Sandboxing](06_sandboxing_.md) keeps your environment secure, how [Streamed Output Processing](07_streamed_output_processing_.md) provides real-time feedback, and how the [Process Registry](08_process_registry_.md) tracks running tasks. Finally, we explored [Checkpointing](09_checkpointing_.md) for versioning your sessions. + +Now, let's look at a feature that allows `claudia` (specifically, the `claude` CLI it controls) to go beyond just interacting with Anthropic's standard Claude API: **MCP (Model Context Protocol)**. + +## The Problem: Connecting to Different AI Sources + +By default, the `claude` CLI is primarily designed to connect to Anthropic's Claude API endpoints (like the ones that power Sonnet, Opus, etc.). But what if you want to use a different AI model? Perhaps a smaller model running locally on your machine, a specialized AI tool you built, or an internal AI service within your company? + +These other AI sources might have different ways of communicating. You need a standard way for `claudia` (or rather, the `claude` CLI it manages) to talk to *any* AI service that can process prompts, use tools, and respond, regardless of who built it or how it runs. + +This is the problem MCP solves. It provides a standardized "language" or "interface" that allows `claude` to communicate with any external program or service that "speaks" MCP. + +Imagine `claudia` is a smart home hub. It needs to talk to various devices – lights, thermostats, speakers – made by different companies. Instead of needing a unique connection method for every single brand, they all agree to use a standard protocol (like Wi-Fi and a common API). MCP is that standard protocol for AI model servers. + +## What is MCP (Model Context Protocol)? + +MCP stands for **Model Context Protocol**. It's a standard protocol used by the `claude` CLI to exchange information with external programs or services that act as AI models or tools. + +When you configure an "MCP Server" in `claude` (and thus in `claudia`), you're telling `claude` about an external AI source that it can connect to using the MCP standard. + +This abstraction layer manages: + +1. **Defining Servers:** Telling `claude` about external MCP sources by giving them a name and specifying how to connect (e.g., run a specific command, connect to a URL). +2. **Listing Servers:** Seeing which MCP servers are configured. +3. **Interacting:** When a session or Agent is configured to use a specific MCP server, the `claude` CLI connects to that server (instead of the default Anthropic API) and uses the MCP to send prompts and receive responses. + +This capability extends `claudia`'s potential far beyond just Anthropic's hosted models, enabling connections to a variety of AI models or services that implement the MCP standard. + +## Key Concepts + +Here are the main ideas behind MCP in `claudia` (and `claude`): + +| Concept | Description | Analogy | +| :---------------- | :----------------------------------------------------------------------------------------------------------- | :------------------------------------------------------ | +| **MCP Server** | An external program or service that speaks the MCP standard and can act as an AI model or provide tools. | A smart device (light, speaker) in a smart home system. | +| **Transport** | How `claude` connects to the MCP Server. Common types are `stdio` (running the server as a command-line process) or `sse` (connecting to a network URL via Server-Sent Events). | How the hub talks to the device (e.g., Wi-Fi, Bluetooth). | +| **Scope** | Where the MCP server configuration is stored. Affects who can see/use it: `user` (all projects), `project` (via `.mcp.json` in the project directory), `local` (only this `claudia` instance's settings, usually linked to a project). | Where you save the device setup (e.g., globally in the app, specific to one room setup). | +| **MCP Configuration** | The details needed to connect to a server: name, transport type, command/URL, environment variables, scope. | The device's settings (name, type, how to connect, what room it's in). | + +## Using MCP in the UI + +`claudia` provides a dedicated section to manage MCP servers. You'll typically find this under "Settings" or a similar menu item. + +The `MCPManager.tsx` component is the main view for this: + +```typescript +// src/components/MCPManager.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Card } from "@/components/ui/card"; +// ... other imports like api, MCPServerList, MCPAddServer, MCPImportExport ... + +export const MCPManager: React.FC = ({ onBack, className }) => { + const [activeTab, setActiveTab] = useState("servers"); // State for the active tab + const [servers, setServers] = useState([]); // State for the list of servers + const [loading, setLoading] = useState(true); + // ... error/toast state ... + + // Load servers when the component mounts + useEffect(() => { + loadServers(); + }, []); + + // Function to load servers from the backend + const loadServers = async () => { + try { + setLoading(true); + // Call the backend command to list servers + const serverList = await api.mcpList(); + setServers(serverList); // Update state + } catch (err) { + console.error("Failed to load MCP servers:", err); + // ... set error state ... + } finally { + setLoading(false); + } + }; + + // Callbacks for child components (Add, List, Import) + const handleServerAdded = () => { + loadServers(); // Refresh the list after adding + setActiveTab("servers"); // Switch back to the list view + // ... show success toast ... + }; + + const handleServerRemoved = (name: string) => { + setServers(prev => prev.filter(s => s.name !== name)); // Remove server from state + // ... show success toast ... + }; + + const handleImportCompleted = (imported: number, failed: number) => { + loadServers(); // Refresh after import + // ... show import result toast ... + }; + + return ( +
{/* Layout container */} + {/* Header with Back button */} +
+ +

MCP Servers

+
+ + {/* Tabs for navigating sections */} + + + Servers + Add Server + Import/Export + + + {/* Server List Tab Content */} + + {/* Using a Card component */} + + + + + {/* Add Server Tab Content */} + + {/* Using a Card component */} + + + + + {/* Import/Export Tab Content */} + + {/* Using a Card component */} + + + + + + {/* ... Toast notifications ... */} +
+ ); +}; +``` + +This main component uses tabs to organize the different MCP management tasks: +* **Servers:** Shows a list of configured servers using the `MCPServerList` component. +* **Add Server:** Provides a form to manually add a new server using the `MCPAddServer` component. +* **Import/Export:** Contains options to import servers (e.g., from a JSON file or Claude Desktop config) or potentially export them, using the `MCPImportExport` component. + +The `MCPServerList.tsx` component simply takes the list of `MCPServer` objects and displays them, grouped by scope (User, Project, Local). It provides buttons to remove or test the connection for each server, calling the relevant `onServerRemoved` or backend test command. + +The `MCPAddServer.tsx` component presents a form where you can enter the details of a new server: name, select the transport type (Stdio or SSE), provide the command or URL, add environment variables, and choose the scope. When you click "Add", it calls the backend `api.mcpAdd` command. + +```typescript +// src/components/MCPAddServer.tsx (Simplified) +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { SelectComponent } from "@/components/ui/select"; +// ... other imports like api ... + +export const MCPAddServer: React.FC = ({ onServerAdded, onError }) => { + const [transport, setTransport] = useState<"stdio" | "sse">("stdio"); + const [serverName, setServerName] = useState(""); + const [commandOrUrl, setCommandOrUrl] = useState(""); + const [scope, setScope] = useState("local"); + // ... state for args, env vars, saving indicator ... + + const handleAddServer = async () => { + if (!serverName.trim() || !commandOrUrl.trim()) { + onError("Name and Command/URL are required"); + return; + } + + try { + // ... set saving state ... + + // Prepare arguments based on transport type + const command = transport === "stdio" ? commandOrUrl : undefined; + const url = transport === "sse" ? commandOrUrl : undefined; + const args = transport === "stdio" ? commandOrUrl.split(/\s+/).slice(1) : []; // Simplified arg parsing + const env = {}; // Simplified env vars + + // Call the backend API command + const result = await api.mcpAdd( + serverName, + transport, + command, + args, + env, + url, + scope + ); + + if (result.success) { + // Reset form and notify parent + setServerName(""); + setCommandOrUrl(""); + setScope("local"); + // ... reset args/env ... + onServerAdded(); + } else { + onError(result.message); // Show error from backend + } + } catch (error) { + onError("Failed to add server"); + console.error("Failed to add MCP server:", error); + } finally { + // ... unset saving state ... + } + }; + + return ( +
+

Add MCP Server

+ setTransport(v as "stdio" | "sse")}> + + Stdio + SSE + + {/* ... Form fields based on transport type (Name, Command/URL, Scope, Env) ... */} + + +
+ ); +}; +``` + +This component collects user input and passes it to the `api.mcpAdd` function, which is a wrapper around the backend Tauri command. + +Once an MCP server is configured, it can potentially be selected as the "model" for an Agent run or an interactive session, although the integration point for selecting MCP servers specifically during session execution might be evolving or limited in the current `claudia` UI compared to standard Anthropic models. The core mechanism is that the `claude` CLI itself is told *which* configured MCP server to use for a task via command-line arguments, rather than connecting directly to Anthropic. + +## How it Works: Under the Hood (Backend) + +The MCP management in `claudia`'s backend (Rust) doesn't re-implement the MCP standard or manage external processes/connections directly for all servers. Instead, it primarily acts as a wrapper around the **`claude mcp`** subcommand provided by the `claude` CLI itself. + +When you use the MCP management features in `claudia`'s UI: + +1. **Frontend Calls Command:** The frontend calls a Tauri command like `mcp_add`, `mcp_list`, or `mcp_remove` ([Chapter 4: Tauri Commands]). +2. **Backend Calls `claude mcp`:** The backend command receives the request and constructs the appropriate command-line arguments for the `claude mcp` subcommand (e.g., `claude mcp add`, `claude mcp list`, `claude mcp remove`). +3. **Backend Spawns Process:** The backend spawns the `claude` binary as a child process, executing it with the prepared `mcp` arguments ([Chapter 5: Claude CLI Interaction]). +4. **`claude` CLI Handles Logic:** The `claude` CLI process receives the `mcp` command and performs the requested action: + * `claude mcp add`: Parses the provided configuration (name, transport, command/URL, scope) and saves it to its own configuration file (usually `~/.claude/mcp.json` for user/local scope, or writes to `.mcp.json` in the project path for project scope). + * `claude mcp list`: Reads its configuration files and prints the list of configured servers to standard output in a specific text format. + * `claude mcp remove`: Removes the specified server from its configuration files. +5. **Backend Captures Output/Status:** `claudia`'s backend captures the standard output and standard error of the `claude mcp` process ([Chapter 7: Streamed Output Processing], though for simple `mcp` commands it's usually just capturing the final output). +6. **Backend Returns Result:** The backend processes the captured output (e.g., parses the list for `mcp list`, checks for success/failure messages for `mcp add`/`remove`) and returns the result back to the frontend. + +For managing project-scoped servers via `.mcp.json`, the backend also contains specific commands (`mcp_read_project_config`, `mcp_save_project_config`) that read and write the `.mcp.json` file directly using Rust's filesystem functions and JSON parsing. This is an alternative way to manage project-specific MCP configurations that doesn't strictly go through the `claude mcp` CLI commands. + +Here's a sequence diagram showing the flow for adding an MCP server using the `mcp_add` command: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI (MCPAddServer.tsx) + participant Backend as Backend Commands (mcp.rs) + participant OS as Operating System + participant ClaudeCLI as claude binary + + User->>Frontend: Fill form & click "Add Server" + Frontend->>Backend: Call mcp_add(name, transport, command, ...) + Backend->>Backend: Construct arguments for "claude mcp add" + Backend->>OS: Spawn process (claude mcp add ...) + OS-->>ClaudeCLI: Start claude binary + ClaudeCLI->>ClaudeCLI: Parse args, update MCP config file (~/.claude/mcp.json or .mcp.json) + ClaudeCLI-->>OS: Process finishes (exit code 0 on success) + OS-->>Backend: Process status & captured output/error + Backend->>Backend: Check status, parse output for result message + Backend-->>Frontend: Return AddServerResult { success, message } + Frontend->>Frontend: Handle result (show toast, refresh list) + Frontend->>User: User sees confirmation/error +``` + +This diagram shows that for server *management* operations (add, list, remove), `claudia` acts as a GUI frontend to the `claude mcp` command-line interface. + +When a session or Agent is configured to *use* one of these registered MCP servers for its AI interactions, the `claude` binary (launched by `claudia` as described in [Chapter 5: Claude CLI Interaction]) is invoked with arguments telling it *which* server to connect to (e.g., `--model mcp:my-server`). The `claude` binary then uses the configuration it previously saved to establish communication with the specified external MCP server using the correct transport (stdio or sse) and protocol. `claudia`'s role during this phase is primarily launching and monitoring the `claude` process, and streaming its output, as covered in previous chapters. + +## Diving into the Backend Code + +Let's look at some snippets from `src-tauri/src/commands/mcp.rs`. + +The helper function `execute_claude_mcp_command` is central to wrapping the CLI calls: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... +use tauri::AppHandle; +use anyhow::{Context, Result}; +use std::process::Command; +use log::info; + +/// Executes a claude mcp command +fn execute_claude_mcp_command(app_handle: &AppHandle, args: Vec<&str>) -> Result { + info!("Executing claude mcp command with args: {:?}", args); + + // Find the claude binary path (logic from Chapter 5) + let claude_path = super::claude::find_claude_binary(app_handle)?; + + // Create a command with inherited environment (helper from Chapter 5) + let mut cmd = super::claude::create_command_with_env(&claude_path); + + cmd.arg("mcp"); // Add the 'mcp' subcommand + for arg in args { + cmd.arg(arg); // Add specific arguments (add, list, remove, get, serve, test-connection, etc.) + } + + // Run the command and capture output + let output = cmd.output() + .context("Failed to execute claude mcp command")?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) // Return stdout on success + } else { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + Err(anyhow::anyhow!("Command failed: {}", stderr)) // Return stderr as error + } +} +``` + +This function simply prepares and runs the `claude mcp ...` command and handles returning the result or error message based on the exit status. + +Now, let's see how `mcp_add` uses this helper: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... + +/// Adds a new MCP server +#[tauri::command] +pub async fn mcp_add( + app: AppHandle, + name: String, + transport: String, + command: Option, + args: Vec, + env: HashMap, + url: Option, + scope: String, +) -> Result { + info!("Adding MCP server: {} with transport: {}", name, transport); + + let mut cmd_args = vec!["add"]; // The 'add' subcommand argument + + // Add arguments for scope, transport, env, name, command/url + // These match the expected arguments for 'claude mcp add' + cmd_args.push("-s"); + cmd_args.push(&scope); + + if transport == "sse" { + cmd_args.push("--transport"); + cmd_args.push("sse"); + } + + for (key, value) in env.iter() { + cmd_args.push("-e"); + cmd_args.push(&format!("{}={}", key, value)); // Format env vars correctly + } + + cmd_args.push(&name); // The server name + + if transport == "stdio" { + if let Some(cmd_str) = &command { + // Handle commands with spaces/args by adding "--" separator if needed + cmd_args.push("--"); + cmd_args.push(cmd_str); + for arg in &args { + cmd_args.push(arg); + } + } else { /* ... error handling ... */ } + } else if transport == "sse" { + if let Some(url_str) = &url { + cmd_args.push(url_str); // The URL for SSE + } else { /* ... error handling ... */ } + } else { /* ... error handling ... */ } + + // Execute the command using the helper + match execute_claude_mcp_command(&app, cmd_args) { + Ok(output) => { + // Parse the output message from claude mcp add + Ok(AddServerResult { + success: true, + message: output.trim().to_string(), + server_name: Some(name), + }) + } + Err(e) => { + // Handle errors from the command execution + Ok(AddServerResult { + success: false, + message: e.to_string(), + server_name: None, + }) + } + } +} +``` + +This command function demonstrates how it builds the `cmd_args` vector, carefully adding the correct flags and values expected by the `claude mcp add` command. It then passes these arguments to `execute_claude_mcp_command` and formats the result into the `AddServerResult` struct for the frontend. + +The `mcp_list` command is similar, executing `claude mcp list` and then parsing the text output (which can be complex, as noted in the code comments) to build the `Vec` structure returned to the frontend. + +Direct file access for `.mcp.json` (project scope) looks like this: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... +use std::path::PathBuf; +use std::fs; +use serde::{Serialize, Deserialize}; + +// Structs mirroring the .mcp.json structure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPProjectConfig { + #[serde(rename = "mcpServers")] + pub mcp_servers: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPServerConfig { + pub command: String, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub env: HashMap, +} + + +/// Reads .mcp.json from the current project +#[tauri::command] +pub async fn mcp_read_project_config(project_path: String) -> Result { + log::info!("Reading .mcp.json from project: {}", project_path); + + let mcp_json_path = PathBuf::from(&project_path).join(".mcp.json"); + + if !mcp_json_path.exists() { + // Return empty config if file doesn't exist + return Ok(MCPProjectConfig { mcp_servers: HashMap::new() }); + } + + match fs::read_to_string(&mcp_json_path) { // Read the file content + Ok(content) => { + match serde_json::from_str::(&content) { // Parse JSON + Ok(config) => Ok(config), + Err(e) => { + log::error!("Failed to parse .mcp.json: {}", e); + Err(format!("Failed to parse .mcp.json: {}", e)) + } + } + } + Err(e) => { + log::error!("Failed to read .mcp.json: {}", e); + Err(format!("Failed to read .mcp.json: {}", e)) + } + } +} + +/// Saves .mcp.json to the current project +#[tauri::command] +pub async fn mcp_save_project_config( + project_path: String, + config: MCPProjectConfig, +) -> Result { + log::info!("Saving .mcp.json to project: {}", project_path); + + let mcp_json_path = PathBuf::from(&project_path).join(".mcp.json"); + + let json_content = serde_json::to_string_pretty(&config) // Serialize config to JSON + .map_err(|e| format!("Failed to serialize config: {}", e))?; + + fs::write(&mcp_json_path, json_content) // Write to the file + .map_err(|e| format!("Failed to write .mcp.json: {}", e))?; + + Ok("Project MCP configuration saved".to_string()) +} +``` + +These commands directly interact with the `.mcp.json` file in the project directory, allowing the UI to edit project-specific configurations without necessarily going through the `claude mcp` command for every change, although `claude` itself will still read this file when run within that project. + +## Conclusion + +In this final chapter, we explored **MCP (Model Context Protocol)**, the standard that allows the `claude` CLI to communicate with external AI model servers running outside the main Claude API. We learned that `claudia` leverages the `claude mcp` subcommand to manage configurations for these external servers, supporting different transport methods (stdio, sse) and scopes (user, project, local). + +We saw how the `claudia` UI provides dedicated sections to list, add, and import MCP servers, and how these actions map to backend Tauri commands. We then looked under the hood to understand that `claudia`'s backend primarily acts as a wrapper, executing `claude mcp` commands to let the `claude` CLI handle the actual configuration management and, during session execution, the communication with the external MCP servers. `claudia` also provides direct file-based management for project-scoped `.mcp.json` configurations. + +Understanding MCP highlights how `claudia` builds a flexible interface on top of `claude`, enabling connections to a potentially diverse ecosystem of AI tools and models that implement this protocol. This extends `claudia`'s capabilities beyond simply interacting with Anthropic's hosted services. + +This concludes our tutorial on the core concepts behind the `claudia` project. We hope this journey through its various components has provided you with a solid understanding of how this application works! + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/mcp.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPAddServer.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPImportExport.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPManager.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPServerList.tsx) diff --git a/Claudia-docs/claudia-1_tab_management_.md b/Claudia-docs/claudia-1_tab_management_.md new file mode 100644 index 00000000..fdb00f39 --- /dev/null +++ b/Claudia-docs/claudia-1_tab_management_.md @@ -0,0 +1,409 @@ +# Chapter 1: Tab Management + +Welcome to Claudia! This tutorial will guide you through the core concepts that make Claudia a powerful and organized tool for your coding projects. We'll start with something you're probably already very familiar with: tabs! + +## What is Tab Management? + +Think about how you use a web browser like Chrome or Firefox. You probably have many tabs open at once, right? One for your email, another for a news article, maybe a few for different research pages. You can easily switch between them, open new ones, and close old ones. This helps you keep multiple tasks organized without having to open many separate browser windows. + +In Claudia, we use the exact same idea! Instead of browser pages, our tabs hold different parts of the application. You might have: + +* A **chat session** where you're asking Claude (our AI assistant) to write code. +* A **project list** to see all your ongoing coding projects. +* A **settings page** to customize Claudia's behavior. +* An **agent execution** view, showing what an AI agent is doing. + +This system of tabs is what we call "Tab Management." It's designed to help you stay organized and switch between different workflows seamlessly. + +## Why is Tab Management Important? + +Imagine you're deeply engrossed in a Claude Code chat session, working on a complex problem. Suddenly, you realize you need to check the overall list of your projects. Without tabs, you might have to leave your current chat, go to a different screen, find your project, and then try to remember where you left off in your chat. That's disruptive! + +With Tab Management, it's easy: you just click on the "CC Projects" tab or open a new one, browse what you need, and then click back to your "Claude Chat" tab. Your chat session is still there, exactly as you left it. It's like having multiple workspaces open at the same time, right at your fingertips. + +## Key Concepts of Claudia's Tab System + +To understand how this works, let's look at a few core ideas: + +1. **Tab:** Each "box" at the top of the Claudia window represents a tab. It's a container for a specific view or activity. +2. **Active Tab:** Only one tab can be "active" at a time. This is the tab whose content you currently see and interact with. It's usually highlighted. +3. **Tab Types:** Not all tabs are the same! A tab can be of different "types" depending on what it's showing: + * `chat`: For interactive Claude Code sessions. + * `projects`: To view your project list and their sessions. + * `settings`: For application settings. + * `agent`: To monitor AI agent runs. + * ...and more! Each type has its own content. +4. **Tab Lifecycle:** Tabs are not static. You can: + * **Create** new tabs. + * **Switch** between existing tabs. + * **Update** a tab's content (e.g., changing its title or showing new data). + * **Close** tabs when you're done with them. + +## How to Use Tabs in Claudia + +Let's see how you'd use these concepts in practice. + +### Opening a New Tab + +You'll often start by wanting to create a new Claude Code chat session or browse your projects. + +**In the User Interface:** + +Look for the `+` button, usually on the right side of your tab bar. Clicking this button will typically open a new "CC Projects" tab, from which you can then start a new Claude Code session. + +**Using Code (for developers):** + +If you were building a new feature in Claudia and wanted to open a new chat tab, you might use a special tool called a "hook" (we'll learn more about hooks later!) called `useTabState`. + +```typescript +import { useTabState } from "@/hooks/useTabState"; + +function MyComponent() { + const { createChatTab } = useTabState(); + + const handleNewChatClick = () => { + // This function creates a new chat tab + createChatTab(); + }; + + return ( + + ); +} +``` +This small snippet, when part of a larger component, means that when `handleNewChatClick` is activated (for example, by clicking a button), a brand new Claude Code chat tab will appear and become the active tab. + +Similarly, to open the "CC Projects" view in a tab (if it's not already open): + +```typescript +import { useTabState } from "@/hooks/useTabState"; + +function TopbarMenu() { + const { createProjectsTab } = useTabState(); + + const openProjects = () => { + // This will open or switch to the CC Projects tab + createProjectsTab(); + }; + + // ... rest of your component +} +``` +Calling `createProjectsTab()` checks if a "CC Projects" tab already exists. If it does, it simply switches to it; otherwise, it creates a new one. This ensures you don't end up with many identical "CC Projects" tabs. + +### Switching Between Tabs + +Once you have multiple tabs, you'll want to move between them. + +**In the User Interface:** + +* **Clicking:** Simply click on the title of the tab you want to switch to in the tab bar. +* **Keyboard Shortcuts:** + * `Ctrl + Tab` (Windows/Linux) or `Cmd + Tab` (macOS): Switches to the next tab. + * `Ctrl + Shift + Tab` (Windows/Linux) or `Cmd + Shift + Tab` (macOS): Switches to the previous tab. + * `Ctrl + 1` through `Ctrl + 9` (or `Cmd + 1` through `Cmd + 9`): Switches directly to the tab at that number position (e.g., `Ctrl + 1` for the first tab). + +**Using Code:** + +If you need to programmatically switch to a specific tab, for instance, after an operation completes in another part of the app: + +```typescript +import { useTabState } from "@/hooks/useTabState"; + +function SessionList({ sessionIdToFocus }) { + const { switchToTab } = useTabState(); + + const handleSessionOpen = (tabId: string) => { + // This will make the tab with `tabId` the active one + switchToTab(tabId); + }; + + // ... imagine logic that gets a tabId and calls handleSessionOpen +} +``` +The `switchToTab` function takes the unique `id` of a tab and makes it the active one, bringing its content to the front. + +### Closing Tabs + +When you're done with a task, you can close its tab. + +**In the User Interface:** + +* **Click the 'X':** Each tab has a small `X` icon next to its title. Click it to close the tab. +* **Keyboard Shortcut:** `Ctrl + W` (Windows/Linux) or `Cmd + W` (macOS): Closes the currently active tab. + +**Using Code:** + +```typescript +import { useTabState } from "@/hooks/useTabState"; + +function TabItem({ tabId }) { + const { closeTab } = useTabState(); + + const handleCloseClick = () => { + // This will close the tab with `tabId` + closeTab(tabId); + }; + + // ... render the 'X' button with onClick={handleCloseClick} +} +``` +The `closeTab` function handles the removal of a tab. If a tab has unsaved changes, it might even ask for confirmation before closing (though this part is not shown in the simple example). + +## Under the Hood: How Tab Management Works + +Now, let's peek behind the curtain and see how Claudia manages all these tabs. + +### The Flow of a New Tab + +When you, for example, click the `+` button to create a new "CC Projects" tab, here's a simplified sequence of what happens: + +```mermaid +sequenceDiagram + participant User + participant App UI (TabManager) + participant useTabState Hook + participant TabContext Store + participant App UI (TabContent) + + User->>App UI (TabManager): Clicks "+" button + App UI (TabManager)->>useTabState Hook: Calls createProjectsTab() + useTabState Hook->>TabContext Store: Calls addTab({type: 'projects', ...}) + Note over TabContext Store: Generates unique ID, adds tab to list, sets as active + TabContext Store-->>useTabState Hook: Returns new tab ID + useTabState Hook->>TabContext Store: Calls setActiveTab(newTabId) + TabContext Store->>App UI (TabManager): Notifies tabs list changed + TabContext Store->>App UI (TabContent): Notifies active tab changed + App UI (TabManager)->>App UI (TabManager): Renders new tab button in tab bar + App UI (TabContent)->>App UI (TabContent): Renders "CC Projects" content +``` + +### The Core Components + +Claudia's tab management is built using a few key pieces: + +1. **`TabContext` (The Brain):** + * **File:** `src/contexts/TabContext.tsx` + * **Purpose:** This is the central "data store" for all tab-related information. It holds the list of all open tabs, which tab is currently active, and the functions to add, remove, and update tabs. + * **Analogy:** Imagine a clipboard or a big whiteboard where all the tabs' information is kept. Any part of the app can look at or change this whiteboard's contents through specific rules. + + Here's a simplified look at how `TabContext` defines a tab and manages its state: + + ```typescript + // src/contexts/TabContext.tsx (simplified) + export interface Tab { + id: string; // Unique identifier for the tab + type: 'chat' | 'agent' | 'projects' | 'settings'; // What kind of content it holds + title: string; // Text shown on the tab button + sessionId?: string; // Optional: specific to chat tabs + status: 'active' | 'idle' | 'running'; // Tab's current state + hasUnsavedChanges: boolean; // Does it need saving? + // ... more properties + } + + export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [tabs, setTabs] = useState([]); + const [activeTabId, setActiveTabId] = useState(null); + + // Effect to create a default "CC Projects" tab when the app starts + useEffect(() => { + const defaultTab: Tab = { + id: `tab-${Date.now()}-...`, // unique ID + type: 'projects', + title: 'CC Projects', + status: 'idle', + hasUnsavedChanges: false, + order: 0, + createdAt: new Date(), + updatedAt: new Date() + }; + setTabs([defaultTab]); + setActiveTabId(defaultTab.id); + }, []); // [] means it runs only once when the component mounts + + // Functions like addTab, removeTab, updateTab, setActiveTab are defined here + // ... + return ( + + {children} + + ); + }; + ``` + The `TabProvider` wraps the entire application (you can see this in `src/App.tsx`), making the `TabContext` available to all parts of the app. It also ensures that Claudia always starts with a "CC Projects" tab. + +2. **`useTabState` (The Helper Hook):** + * **File:** `src/hooks/useTabState.ts` + * **Purpose:** This is a special "hook" that makes it super easy for any part of Claudia's user interface to talk to the `TabContext`. Instead of directly messing with `TabContext`, components use `useTabState` to create tabs, switch them, or get information about them. + * **Analogy:** If `TabContext` is the big whiteboard, `useTabState` is like your personal assistant who reads from and writes on the whiteboard for you, so you don't have to get your hands dirty. + + Here's how `useTabState` provides handy functions: + + ```typescript + // src/hooks/useTabState.ts (simplified) + import { useTabContext } from '@/contexts/TabContext'; + + export const useTabState = () => { + const { + tabs, activeTabId, addTab, removeTab, updateTab, setActiveTab + } = useTabContext(); + + // Example: A helper function to create a chat tab + const createChatTab = useCallback((projectId?: string, title?: string): string => { + const tabTitle = title || `Chat ${tabs.length + 1}`; + return addTab({ // Calls addTab from TabContext + type: 'chat', + title: tabTitle, + sessionId: projectId, + status: 'idle', + hasUnsavedChanges: false, + }); + }, [addTab, tabs.length]); // Dependencies for useCallback + + // Example: A helper function to close a tab + const closeTab = useCallback(async (id: string, force: boolean = false): Promise => { + // ... logic to check for unsaved changes ... + removeTab(id); // Calls removeTab from TabContext + return true; + }, [removeTab]); + + // Returns all the useful functions and state for components + return { + tabs, activeTabId, createChatTab, closeTab, + switchToTab: setActiveTab, // Renaming setActiveTab for clarity + // ... more functions + }; + }; + ``` + +3. **`TabManager` (The Tab Bar UI):** + * **File:** `src/components/TabManager.tsx` + * **Purpose:** This component is responsible for drawing the visual tab bar at the top of the application. It shows each tab's title, icon, close button, and highlights the active tab. It also handles the `+` button for creating new tabs. + * **Analogy:** This is the physical row of tabs on your web browser's window frame. + + ```typescript + // src/components/TabManager.tsx (simplified) + import { useTabState } from '@/hooks/useTabState'; + import { X, Plus, Folder } from 'lucide-react'; // Icons + + export const TabManager: React.FC = () => { + const { + tabs, activeTabId, createProjectsTab, closeTab, switchToTab, canAddTab + } = useTabState(); // Uses our helper hook! + + const handleNewTab = () => { + if (canAddTab()) { + createProjectsTab(); // Calls the function from useTabState + } + }; + + return ( +
+ {/* Loop through all 'tabs' to render each one */} + {tabs.map((tab) => ( +
switchToTab(tab.id)} // Switches tab on click + > + {/* Example icon */} + {tab.title} + +
+ ))} + +
+ ); + }; + ``` + This component directly uses `useTabState` to get the list of tabs, know which one is active, and call functions like `createProjectsTab`, `closeTab`, and `switchToTab` when the user interacts with the UI. + +4. **`TabContent` (The View Area):** + * **File:** `src/components/TabContent.tsx` + * **Purpose:** This component is where the actual content of the active tab is displayed. It looks at the `activeTabId` from `useTabState` and then renders the correct component for that tab's `type` (e.g., `ClaudeCodeSession` for a `chat` tab, `ProjectList` for a `projects` tab). + * **Analogy:** This is the main part of your web browser window where the actual web page content appears. + + ```typescript + // src/components/TabContent.tsx (simplified) + import React, { Suspense, lazy } from 'react'; + import { useTabState } from '@/hooks/useTabState'; + import { Loader2 } from 'lucide-react'; + + // Lazily load components for performance + const ClaudeCodeSession = lazy(() => import('@/components/ClaudeCodeSession').then(m => ({ default: m.ClaudeCodeSession }))); + const ProjectList = lazy(() => import('@/components/ProjectList').then(m => ({ default: m.ProjectList }))); + const Settings = lazy(() => import('@/components/Settings').then(m => ({ default: m.Settings }))); + + interface TabPanelProps { + tab: Tab; + isActive: boolean; + } + + const TabPanel: React.FC = ({ tab, isActive }) => { + // Determines which component to show based on tab.type + const renderContent = () => { + switch (tab.type) { + case 'projects': + // ProjectList needs data, so it fetches it internally or gets it from context + return ; + case 'chat': + return ; + case 'settings': + return ; + // ... other tab types + default: + return
Unknown tab type: {tab.type}
; + } + }; + + return ( +
+ }> + {renderContent()} + +
+ ); + }; + + export const TabContent: React.FC = () => { + const { tabs, activeTabId } = useTabState(); // Gets tabs and activeTabId + + return ( +
+ {/* Render a TabPanel for each tab, but only show the active one */} + {tabs.map((tab) => ( + + ))} + {tabs.length === 0 && ( +
+

No tabs open. Click the + button to start a new chat.

+
+ )} +
+ ); + }; + ``` + `TabContent` ensures that only the content for the `activeTabId` is visible, effectively "switching" the view when you click on a different tab. + +In summary, `TabContext` manages the data, `useTabState` provides an easy way for components to interact with that data, `TabManager` displays the tab bar, and `TabContent` displays the content of the currently selected tab. + +## Conclusion + +You've now learned about Claudia's powerful Tab Management system! You understand why it's crucial for keeping your workflows organized, how to interact with tabs via the UI and (if you're a developer) through code, and the main components working behind the scenes. This multi-tab interface allows you to effortlessly navigate between your coding projects, AI chat sessions, agent executions, and application settings, providing a smooth and efficient user experience. + +In the next chapter, we'll dive deeper into one of the most exciting features you can use within these tabs: [Claude Code Session](02_claude_code_session_.md). You'll learn how to start interacting with Claude and manage your AI-powered coding sessions. + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/App.tsx), [[2]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/components/App.cleaned.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/components/TabContent.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/components/TabManager.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/contexts/TabContext.tsx), [[6]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/hooks/useTabState.ts) +```` \ No newline at end of file diff --git a/Claudia-docs/claudia-2_claude_code_session_.md b/Claudia-docs/claudia-2_claude_code_session_.md new file mode 100644 index 00000000..ad9d6b23 --- /dev/null +++ b/Claudia-docs/claudia-2_claude_code_session_.md @@ -0,0 +1,633 @@ +# Chapter 2: Claude Code Session + +In the last chapter, you learned how Claudia uses [Tab Management](01_tab_management_.md) to keep your workspace organized. You discovered that each tab can hold different parts of the application, including a "chat" tab. Now, let's dive into what that "chat" tab truly represents: the **Claude Code Session**. + +## What is a Claude Code Session? + +Imagine you're working on a coding project and you need a super-smart assistant right by your side, who can not only chat with you but also understand your code, make changes to files, and even run commands in your project's terminal. That's exactly what a Claude Code Session is! + +It's a specialized, interactive chat window built into Claudia. Instead of just talking, you're collaborating with Claude (our AI assistant) directly on your code. You give it instructions, Claude responds, and its actions—like editing files, running tests, or listing directory contents—are displayed right there in the conversation. + +**The main problem it solves:** How do you effectively integrate an advanced AI into your everyday coding workflow? A Claude Code Session provides that seamless interface, making AI-assisted development feel natural and intuitive. + +**Think of it like this:** You're sitting next to a highly skilled co-worker. You tell them what you want to achieve, they might ask clarifying questions, then they show you the code they've written, perhaps explaining their choices. If it's not quite right, you give more feedback, and they adjust. The Claude Code Session is your digital version of this collaborative experience. + +### A Common Scenario: Fixing a Bug + +Let's say you're working on a web application, and you've identified a bug in a specific file. Here's how a Claude Code Session helps: + +1. **You:** "Hey Claude, I have a bug in `src/utils/dateFormatter.ts`. It's incorrectly formatting dates for users in the Asia/Tokyo timezone. Can you take a look and fix it?" +2. **Claude:** (Reads the file, analyzes the code, possibly runs a test) "Okay, I see the issue. It looks like the `Intl.DateTimeFormat` options are not correctly handling the `timeZone` property. I'll propose an edit." (Displays a suggested code change) +3. **You:** "Looks good. Apply that change." +4. **Claude:** (Applies the change, confirms) "Done. Would you like me to run the tests to verify the fix?" +5. **You:** "Yes, please." + +This back-and-forth, with Claude directly interacting with your project, is the core of a Claude Code Session. + +## Key Concepts of a Claude Code Session + +To get the most out of your coding partnership with Claude, it's helpful to understand the main ideas behind a session: + +1. **Interactive Conversation (Chat Interface):** + * It's a live chat. You type messages (prompts), and Claude sends messages back. + * The entire conversation history is visible, so you can always see the context of your interaction. + +2. **Coding Environment & Context:** + * Unlike a general chatbot, Claude in a session is always aware of your **project path**. This means it knows which files and folders it can see and work with. + * Its responses and actions are tailored to the code within that specific project. + +3. **Input Prompt:** + * This is the text box at the bottom of the session where you type your instructions, questions, or feedback for Claude. + * It's your primary way to guide the AI. + +4. **Streaming Output:** + * Claude's responses don't just appear all at once. They "stream" in real-time, character by character. This makes the interaction feel very dynamic, as if Claude is typing live. + * You'll see not just text, but also visual indicators of its actions (like running commands or making edits). + +5. **Tool Uses:** + * Claude isn't magic; it uses specific "tools" to interact with your project. These tools are like mini-programs or functions that let Claude: + * `read`: Read the content of a file. + * `write`: Create or modify a file. + * `edit`: Apply specific changes to a file (like adding or deleting lines). + * `bash`: Run commands in your terminal (e.g., `npm test`, `ls`). + * `ls`: List files and directories. + * ...and many more! + * When Claude uses a tool, you'll see a clear display showing which tool it used and what its input was. This transparency helps you understand what Claude is doing. + +6. **Session Continuation & Resumption:** + * One of the best features is that your sessions are saved! You can close Claudia, or switch to a different tab, and when you return to a Claude Code Session tab, the entire conversation and project context is restored exactly as you left it. + * This allows you to take breaks, work on multiple tasks, and easily pick up where you left off. + +## How to Use a Claude Code Session in Claudia + +Let's walk through starting and interacting with a Claude Code Session. + +### 1. Starting a New Session + +You'll typically start a new session from the "CC Projects" tab. + +**In the User Interface:** + +1. If you don't have a "CC Projects" tab open, click the `+` button in the tab bar (as learned in [Tab Management](01_tab_management_.md)). +2. In the "CC Projects" view, you'll see a button like "Start new Claude Code session" or you can click on an existing project to resume a session within it. +3. Once a new Claude Code Session tab opens, the first thing you'll need to do is **select your project directory**. This tells Claude where your code lives! + + You'll see an input field for "Project Directory" and a folder icon. Click the folder icon to browse your computer and select the root folder of your coding project. + + ![Select Project Path](https://i.imgur.com/example-select-path.png) + + Once selected, the path will appear in the input field. + +### 2. Giving Your First Prompt + +With your project path set, you're ready to talk to Claude! + +At the bottom of the session, you'll see the **prompt input area**. This is where you type your instructions. + +```typescript +// src/components/FloatingPromptInput.tsx (simplified) + +interface FloatingPromptInputProps { + onSend: (prompt: string, model: "sonnet" | "opus") => void; + // ... other props +} + +const FloatingPromptInputInner = (props: FloatingPromptInputProps, ref: React.Ref) => { + const [prompt, setPrompt] = useState(""); + // ... other state and refs + + const handleSend = () => { + if (prompt.trim()) { + // Calls the onSend function passed from the parent component + props.onSend(prompt.trim(), selectedModel); + setPrompt(""); // Clear the input after sending + } + }; + + return ( +
+ {/* Model Picker, Thinking Mode Picker */} +
+